An interactive walkthrough of how invisible Unicode characters hide malicious code inside VS Code extensions — and how to detect them.
In a real attack, this would steal your GitHub tokens, npm credentials, or drain crypto wallets. For this demo, we'll use a harmless payload:
The goal: hide this code inside a VS Code extension file so that no human or editor can see it, but JavaScript can still execute it.
How? Every character is just a number. And there are Unicode characters that map to numbers but render as absolutely nothing on screen.
Each character in our payload has a numeric value. The letter 'c' is byte 0x63, the letter 'o' is 0x6F, and so on.
| Char | Decimal | Hex | Binary |
|---|---|---|---|
| 'c' | 99 | 0x63 | 01100011 |
| 'o' | 111 | 0x6F | 01101111 |
| 'n' | 110 | 0x6E | 01101110 |
| 's' | 115 | 0x73 | 01110011 |
| 'o' | 111 | 0x6F | 01101111 |
| 'l' | 108 | 0x6C | 01101100 |
| 'e' | 101 | 0x65 | 01100101 |
| '.' | 46 | 0x2E | 00101110 |
GlassWorm needs to represent each of these byte values using only invisible characters. That's where the nibble split comes in.
A nibble is half a byte — 4 bits. It can represent values 0 through 15, which is 0x0 to 0xF. This matters because Unicode gives us exactly 16 invisible characters to work with — one for each nibble value.
Unicode has 16 characters called Variation Selectors (U+FE00 to U+FE0F). They were designed to modify emoji rendering, but your editor draws absolutely nothing for them. Add 0xFE00 to a nibble, and you get an invisible character.
| Nibble | + 0xFE00 | Result | Renders as |
|---|---|---|---|
| 0 | 0 + 0xFE00 | U+FE00 | nothing (invisible) |
| 1 | 1 + 0xFE00 | U+FE01 | nothing (invisible) |
| 2 | 2 + 0xFE00 | U+FE02 | nothing (invisible) |
| 3 | 3 + 0xFE00 | U+FE03 | nothing (invisible) |
| 4 | 4 + 0xFE00 | U+FE04 | nothing (invisible) |
| 5 | 5 + 0xFE00 | U+FE05 | nothing (invisible) |
| 6 | 6 + 0xFE00 | U+FE06 | nothing (invisible) |
| 7 | 7 + 0xFE00 | U+FE07 | nothing (invisible) |
| ... 8 through 15 follow the same pattern (U+FE08 – U+FE0F) | |||
So the letter 'c' (nibbles 6, 3) becomes U+FE06 + U+FE03 — two characters that are physically in the file but completely invisible to your eyes.
The invisible payload is injected between normal lines of code. Here's what a code reviewer sees in VS Code vs what's actually in the file:
Line 2 appears completely empty. But it contains 82 invisible variation selector characters encoding the full malicious payload. The file is 246 bytes heavier — but visually identical.
A small, visible JavaScript function reads the invisible characters and reconstructs the original payload. It looks like a harmless Unicode utility — but it's the weapon.
For our letter 'c': read U+FE06 → subtract 0xFE00 → get 6. Read U+FE03 → subtract 0xFE00 → get 3. Shift and combine: (6 << 4) | 3 = 0x63 = 'c'. Done.
Type anything below and watch it get encoded into invisible characters in real time. Then decode it back to prove the round-trip works.
The attack is clever, but the detection is straightforward: count variation selectors. Legitimate code has 0–2 of them. GlassWorm payloads have hundreds.
0xFE00 or codePointAt near invisible chars → decoder foundeval() + string construction → payload execution pattern~/.vscode/extensions/ with the scannerglassworm-hunter, anti-trojan-source, or the scanner from this toolkit
Created by Visagan S — Security Engineer & Pentester
Educational tool. The payloads used are harmless.
References: Koi Security, Aikido, Snyk, Veracode