Zero-width Unicode obfuscation exploits invisible characters from the Unicode standard to encode malicious payloads within seemingly empty text. Unlike traditional encoding methods that produce visible output, zero-width obfuscation renders payloads completely invisible to human reviewers and most static analysis tools. First weaponised in targeted phishing campaigns in early 2025, the technique reached critical mass with the GlassWorm supply chain worm in October 2025, which compromised over 35,000 VSCode installations using invisible code injection.
You are staring at a blank line in a code review. The diff shows whitespace changes. You approve the merge. You just deployed a remote access trojan. Koi Security documented exactly this pattern in October 2025: seven compromised VSCode extensions on the OpenVSX marketplace, malicious code encoded in Unicode characters that render as empty space. Reviewers saw nothing because there was nothing to see. You already understand zero-width obfuscation conceptually. What you need are detection patterns you can deploy, queries you can run, and response procedures you can execute. That is what follows.
The Invisible Character Taxonomy
Unicode defines multiple character categories that occupy no visual space. Attackers have weaponised each differently. Knowing which characters map to which attack techniques determines what you search for.
| Character Class | Code Points | Attack Technique | First Observed | Detection Priority |
|---|---|---|---|---|
| Hangul Fillers | U+3164, U+FFA0 | Binary payload encoding | October 2024 | Critical |
| Zero-Width Spaces | U+200B, U+200C, U+200D | Binary encoding, URL obfuscation | January 2019 (Z-WASP) | Critical |
| Variation Selectors | U+FE00-FE0F | Nibble encoding (4x denser) | October 2025 (GlassWorm) | Critical |
| BiDi Overrides | U+202A-U+202E, U+2066-U+2069 | Source code logic manipulation | November 2021 (Trojan Source) | High |
| Soft Hyphen | U+00AD | Email header obfuscation | January 2025 (Shy Z-WASP) | Medium |
| Braille Blank | U+2800 | Extension masquerading | August 2024 (Void Banshee) | Medium |
| Tag Characters | U+E0000-U+E007F | LLM prompt injection | January 2024 | Emerging |
The Hangul filler technique dominates current attacks. The Halfwidth Hangul Filler (U+FFA0) represents binary zero; the Hangul Filler (U+3164) represents binary one. Eight invisible characters encode one ASCII byte. This is the encoding used in Tycoon 2FA phishing kits and the Juniper-documented PAC campaign.
GlassWorm introduced Variation Selector encoding, which achieves higher density by mapping each selector to a nibble (4 bits) rather than a single bit. This reduces payload size by 50% compared to binary encoding, making detection based on file size anomalies less reliable.
Detection Patterns You Can Deploy Today
The gap in existing guidance is not explanation. It is implementation. Here are patterns you can use immediately.
Command-Line Detection
Find files containing invisible characters in any codebase:
# Detect Hangul fillers (most common attack vector)
grep -rP '[\x{3164}\x{FFA0}]' --include="*.js" --include="*.ts" --include="*.py" .
# Detect zero-width spaces and joiners
grep -rP '[\x{200B}-\x{200D}\x{2060}\x{FEFF}]' --include="*.js" --include="*.ts" .
# Detect variation selectors (GlassWorm technique)
grep -rP '[\x{FE00}-\x{FE0F}]' --include="*.js" --include="*.ts" .
# Detect BiDi overrides (Trojan Source)
grep -rP '[\x{202A}-\x{202E}\x{2066}-\x{2069}]' .
# Comprehensive scan - all suspicious invisible characters
grep -rP '[\x{200B}-\x{200D}\x{2060}\x{FEFF}\x{3164}\x{FFA0}\x{00AD}\x{2800}\x{202A}-\x{202E}\x{FE00}-\x{FE0F}]' .For large repositories, ripgrep provides significantly better performance:
rg '[\u200B-\u200D\u2060\uFEFF\u3164\uFFA0\u00AD\u2800\u202A-\u202E\uFE00-\uFE0F]' --type js --type ts --type pyGit Pre-Commit Hook
Block commits containing invisible characters before they enter your repository:
#!/bin/bash
# .git/hooks/pre-commit
INVISIBLE_PATTERN='[\x{200B}-\x{200D}\x{2060}\x{FEFF}\x{3164}\x{FFA0}\x{00AD}\x{2800}\x{202A}-\x{202E}\x{FE00}-\x{FE0F}]'
if git diff --cached --name-only | xargs grep -lP "$INVISIBLE_PATTERN" 2>/dev/null; then
echo "ERROR: Commit blocked. Invisible Unicode characters detected."
echo "Files containing suspicious characters:"
git diff --cached --name-only | xargs grep -lP "$INVISIBLE_PATTERN"
echo ""
echo "If these are intentional (e.g., internationalisation files), use:"
echo " git commit --no-verify"
exit 1
fiGitHub Actions Workflow
Scan pull requests automatically in CI/CD:
name: Invisible Character Scan
on: [pull_request]
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Scan for invisible Unicode
run: |
FOUND=$(grep -rP '[\x{200B}-\x{200D}\x{2060}\x{FEFF}\x{3164}\x{FFA0}\x{00AD}\x{2800}\x{202A}-\x{202E}\x{FE00}-\x{FE0F}]' \
--include="*.js" --include="*.ts" --include="*.py" --include="*.jsx" --include="*.tsx" \
. 2>/dev/null || true)
if [ -n "$FOUND" ]; then
echo "::error::Invisible Unicode characters detected in source files"
echo "$FOUND"
exit 1
fi
echo "No invisible characters detected"YARA Rule
For malware analysis pipelines and EDR integration:
rule Invisible_Unicode_Payload {
meta:
description = "Detects invisible Unicode character encoding used in malware"
author = "KlaroSkope"
reference = "GlassWorm, Tycoon 2FA, Juniper PAC campaigns"
severity = "high"
strings:
// Hangul filler binary encoding: U+3164 (\xe3\x85\xa4) and U+FFA0 (\xef\xbe\xa0)
$hangul = /(\xe3\x85\xa4|\xef\xbe\xa0){8,}/ // 8+ invisible chars = 1+ encoded bytes
// ZWS/ZWNJ binary pattern: U+200B-U+200D
$zws = /[\xe2\x80\x8b-\xe2\x80\x8d]{16,}/ // UTF-8 encoded
// Variation selectors (GlassWorm): U+FE00-U+FE0F
$vs = /[\xef\xb8\x80-\xef\xb8\x8f]{8,}/ // UTF-8 encoded
// BiDi override sequences (Trojan Source): U+202A-U+202E
$bidi = /[\xe2\x80\xaa-\xe2\x80\xae]/ // Single instance is suspicious
condition:
any of them
}Splunk SPL Query
For SIEM environments ingesting source code or log data:
index=code_repos sourcetype=git_commits
| eval has_hangul=if(match(_raw, "[\x{3164}\x{FFA0}]+"), 1, 0)
| eval has_zws=if(match(_raw, "[\x{200B}-\x{200D}]+"), 1, 0)
| eval has_bidi=if(match(_raw, "[\x{202A}-\x{202E}]+"), 1, 0)
| where has_hangul=1 OR has_zws=1 OR has_bidi=1
| table _time, repo, author, commit_hash, file_path
| sort -_timeReal-World Attack Chains
Understanding documented campaigns helps calibrate detection priorities. Each represents confirmed compromise, not theoretical risk.
What made GlassWorm unprecedented was its command-and-control infrastructure. Primary C2 instructions are encoded in Solana blockchain transaction memos. Because blockchain transactions are immutable, this infrastructure cannot be taken down. Backup C2 uses Google Calendar event titles. The combination creates what researchers termed "unkillable infrastructure."
Detection indicators: Variation Selector sequences in JavaScript files, HTTP requests with unusual response header patterns, Solana RPC API calls from developer workstations, unexpected Calendar API access.
This technique defeats static analysis because the malicious code does not exist in parseable form until runtime. The Proxy mechanism provides legitimate-seeming indirection.
Detection indicators: Hangul filler sequences in HTML/JavaScript, Proxy object instantiation patterns, indirect eval calls, anti-debugging checks (timing-based breakpoint detection).
Detection indicators: Any BiDi override character in source code outside of string literals or comments in explicitly RTL-supporting applications.
Multi-Layer Nesting Patterns
Zero-width encoding is never the final obfuscation layer. Analysis of documented campaigns reveals consistent nesting patterns that inform deobfuscation workflows.
The outermost layer is always zero-width encoding. This provides the stealth that bypasses visual inspection. Inner layers use traditional techniques that would be obvious if not wrapped in invisibility.
Base64 appears as the second layer in approximately 90% of samples. The decoded invisible content is almost always valid Base64. This provides character set normalisation for downstream processing.
Encryption or XOR typically appears as the third layer. Tycoon 2FA campaigns use CryptoJS for AES. GlassWorm uses AES-256-CBC with external key delivery. Simpler campaigns use single-byte XOR with predictable keys (0x17, 0x5A are common).
Compression occasionally appears within the chain, typically gzip. This reduces the size anomaly created by the 8:1 character expansion of binary encoding.
For analysis: decode invisible characters first, then check for Base64 signatures, then evaluate for XOR or additional encoding. Automated deobfuscation handles XOR with known or brute-forceable keys. Strong encryption (AES, RC4, ChaCha) represents a terminus. You can identify the encrypted blob and its likely algorithm, but decryption requires key recovery through memory forensics, traffic analysis, or reverse engineering the key derivation logic.
Response Procedures
If you detect invisible Unicode encoding in your environment, the presence of the technique indicates intentional obfuscation. Legitimate code does not accidentally contain Hangul filler sequences.
Why This Matters Now
The trajectory from Martin Kleppe's October 2024 proof-of-concept to GlassWorm's October 2025 supply chain compromise took twelve months. In that time, the technique moved from academic curiosity to active exploitation by multiple threat actors across phishing, supply chain, and targeted intrusion campaigns.
No CISA advisory addresses invisible Unicode attacks. No NIST guidance covers detection. The security community is building defences ad hoc, sharing techniques through blog posts and conference talks rather than standardised frameworks.
Organisations without explicit detection for invisible characters have a blind spot that sophisticated attackers are actively exploiting. Implementing detection now places you ahead of most defenders and ahead of eventual regulatory guidance.
KlaroSkope automatically detects and decodes invisible Unicode obfuscation across all documented encoding schemes, revealing hidden payloads through nested Base64, hex, URL encoding, and XOR layers. Try KlaroSkope Free →
Frequently Asked Questions
What are the most common invisible characters used in malware?
How do I detect invisible Unicode characters in my codebase?
What is GlassWorm and why is it significant?
Can static analysis tools detect invisible Unicode obfuscation?
What should I do if I find invisible Unicode encoding in code I'm analysing?
Continue Learning
Ready to decode?
See KlaroSkope transform obfuscated scripts into actionable intelligence.
Try It Free