GlassWorm

An interactive walkthrough of how invisible Unicode characters hide malicious code inside VS Code extensions — and how to detect them.

by Visagan S — Security Engineer & Pentester
Scroll to begin
Step 01

The attacker starts with malicious JavaScript

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:

console.log("Hello from invisible code!")

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.

Step 02

Every character is a byte

Each character in our payload has a numeric value. The letter 'c' is byte 0x63, the letter 'o' is 0x6F, and so on.

CharDecimalHexBinary
'c'990x6301100011
'o'1110x6F01101111
'n'1100x6E01101110
's'1150x7301110011
'o'1110x6F01101111
'l'1080x6C01101100
'e'1010x6501100101
'.'460x2E00101110

GlassWorm needs to represent each of these byte values using only invisible characters. That's where the nibble split comes in.

Step 03

Split each byte into two nibbles

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.

c
0x63 → 01100011
↓ split in half
0110 → 6
0011 → 3
byte >> 4 = 6  |  byte & 0x0F = 3
H
0x48 → 01001000
↓ split in half
0100 → 4
1000 → 8
byte >> 4 = 4  |  byte & 0x0F = 8
!
0x21 → 00100001
↓ split in half
0010 → 2
0001 → 1
byte >> 4 = 2  |  byte & 0x0F = 1
Step 04

Map each nibble to an invisible character

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+ 0xFE00ResultRenders as
00 + 0xFE00U+FE00nothing (invisible)
11 + 0xFE00U+FE01nothing (invisible)
22 + 0xFE00U+FE02nothing (invisible)
33 + 0xFE00U+FE03nothing (invisible)
44 + 0xFE00U+FE04nothing (invisible)
55 + 0xFE00U+FE05nothing (invisible)
66 + 0xFE00U+FE06nothing (invisible)
77 + 0xFE00U+FE07nothing (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.

Step 05

What the infected file looks like

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:

What you see in VS Code
1import * as vscode from 'vscode';
2
3export function activate(ctx) {
4  console.log('activated');
5}
What's actually in the file
1import * as vscode from 'vscode';
2⚠ 82 INVISIBLE CHARS
3export function activate(ctx) {
4  console.log('activated');
5}

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.

Step 06

The decoder reverses it all

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.

1
Read
str.codePointAt(i) → gets the invisible char's code point
2
Subtract
codePoint - 0xFE00 → recovers the nibble (0–15)
3
Combine
(highNibble << 4) | lowNibble → rebuilds the original byte
4
Convert
String.fromCharCode(byte) → back to the original character
5
Execute
eval(recoveredString) → 💥 the hidden code runs!

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.

Step 07

Try it yourself — live encoder

Type anything below and watch it get encoded into invisible characters in real time. Then decode it back to prove the round-trip works.

▶ ◀ (click Encode)
Step 08

How to detect and prevent it

The attack is clever, but the detection is straightforward: count variation selectors. Legitimate code has 0–2 of them. GlassWorm payloads have hundreds.

Detection rules (what the scanner checks)
1
Core
3+ variation selectors on one line → suspicious
10+ in a JS/TS file → critical (almost certainly a payload)
2
Decoder
0xFE00 or codePointAt near invisible chars → decoder found
3
Exec
eval() + string construction → payload execution pattern
4
C2
Solana RPC / Google Calendar API in non-blockchain code → C2 channel
Prevention checklist:
→ Disable VS Code extension auto-update
→ Maintain an extension allowlist
→ Scan ~/.vscode/extensions/ with the scanner
→ Add the pre-commit hook to all repos
→ Integrate Unicode scanning into your CI/CD pipeline
→ Use tools: glassworm-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