|
| 1 | +# Glassglyph Scanner |
| 2 | + |
| 3 | +**A reference scanner for invisible unicode and homoglyph attacks on text-based systems.** |
| 4 | + |
| 5 | +In March 2026, the Glassworm campaign compromised 151+ GitHub repositories, npm packages, and VS Code extensions by smuggling malicious code inside invisible Unicode characters — text that renders as zero pixels in every editor, terminal, code review tool, and browser. The same class of attack works against RAG pipelines, LLM agents, email, chat, and any system that ingests text and later retrieves it as context. |
| 6 | + |
| 7 | +Glassglyph Scanner scans text for three attack classes: |
| 8 | + |
| 9 | +1. **Invisible unicode encoding** — Glassworm's substitution cipher (variation selectors U+FE00–FE0F and U+E0100–E01EF) plus tag characters that map 1:1 to printable ASCII. |
| 10 | +2. **Bidi override attacks** — control characters that visually reorder text (`safe.txt` → `safe[RLO]txt.exe`). |
| 11 | +3. **Homoglyph substitution** — Cyrillic and Greek characters replacing visually identical Latin letters (`аnthropic.com` with Cyrillic `а`). |
| 12 | + |
| 13 | +Cost: **under a millisecond per document**. Pure string iteration, zero external dependencies in the core library, zero network I/O, zero LLM inference. |
| 14 | + |
| 15 | +--- |
| 16 | + |
| 17 | +## Try it in 30 seconds |
| 18 | + |
| 19 | +**Option 1 — Docker:** |
| 20 | + |
| 21 | +```bash |
| 22 | +git clone https://github.com/pringlized/glassglyph-scanner.git |
| 23 | +cd glassglyph-scanner |
| 24 | +docker compose up -d |
| 25 | +curl -X POST http://localhost:8080/scan \ |
| 26 | + -H 'Content-Type: application/json' \ |
| 27 | + -d '{"content":"visit dоcs.аnthropic.com for the API"}' |
| 28 | +``` |
| 29 | + |
| 30 | +Expected response: the scanner flags the Cyrillic `о` and `а` as a medium-severity homoglyph finding. |
| 31 | + |
| 32 | +**Option 2 — Python:** |
| 33 | + |
| 34 | +```bash |
| 35 | +pip install -e '.[all]' |
| 36 | +python -c "from glassglyph_scanner import sanitize; r = sanitize('visit dоcs.аnthropic.com'); print(r.findings[0])" |
| 37 | +``` |
| 38 | + |
| 39 | +**Option 3 — CLI:** |
| 40 | + |
| 41 | +```bash |
| 42 | +pip install -e '.[cli]' |
| 43 | +echo 'visit dоcs.аnthropic.com' | glassglyph-scanner scan - |
| 44 | +# exit 1 → findings present (flagged, not blocked) |
| 45 | + |
| 46 | +echo 'plain text' | glassglyph-scanner scan - |
| 47 | +# exit 0 → clean |
| 48 | +``` |
| 49 | + |
| 50 | +--- |
| 51 | + |
| 52 | +## Why this matters |
| 53 | + |
| 54 | +Traditional supply-chain attacks need a decoder at the execution site. **RAG pipelines are different — the LLM is both target and decoder.** When a poisoned item is retrieved as agent context: |
| 55 | + |
| 56 | +1. The agent reads it as knowledge |
| 57 | +2. LLMs tokenize at the byte level — invisible characters are not invisible to the model |
| 58 | +3. The model may follow instructions encoded in them |
| 59 | +4. The item was embedded and clustered with legitimate knowledge, so it has full semantic credibility |
| 60 | + |
| 61 | +**Ingestion-time scanning is the only viable enforcement point.** Once the content is embedded, it's semantically indistinguishable from clean knowledge. |
| 62 | + |
| 63 | +See [`docs/threat-model.md`](docs/threat-model.md) for the full threat model. |
| 64 | + |
| 65 | +--- |
| 66 | + |
| 67 | +## Detection rules |
| 68 | + |
| 69 | +| Range | Name | Severity | Action | |
| 70 | +|---|---|---|---| |
| 71 | +| `U+FE00`–`U+FE0F` | Variation selectors | **Critical** | Block | |
| 72 | +| `U+E0100`–`U+E01EF` | Supp. variation selectors | **Critical** | Block | |
| 73 | +| `U+E0020`–`U+E007F` | Tag characters | **Critical** | Block | |
| 74 | +| `U+200B`–`U+200F` | Zero-width / bidi marks | High | Strip | |
| 75 | +| `U+202A`–`U+202E` | Bidi overrides | High | Strip | |
| 76 | +| `U+2060`–`U+2064` | Invisible math operators | High | Strip | |
| 77 | +| `U+FEFF` (non-BOM position) | Zero-width no-break space | High | Strip | |
| 78 | +| `U+E0001` | Language tag (deprecated) | High | Strip | |
| 79 | +| Mixed-script word w/ confusable | e.g. Cyrillic `а` in `аnthropic` | Medium | Flag | |
| 80 | +| Mixed-script word w/o confusable | e.g. Cyrillic `ж` in `aжb` | Low | Flag | |
| 81 | + |
| 82 | +**Critical = block:** no legitimate text contains these ranges. Their presence indicates an encoding attack. Reject the document. |
| 83 | + |
| 84 | +**High = strip:** these characters have narrow legitimate uses (Arabic text shaping, emoji sequences) but are dangerous in knowledge items. Characters are removed, the document proceeds with sanitized text. |
| 85 | + |
| 86 | +**Medium/Low = flag:** homoglyphs can appear in legitimate multilingual content. The finding is reported; the calling system decides whether to quarantine. |
| 87 | + |
| 88 | +Full rule reference in [`docs/detection-rules.md`](docs/detection-rules.md). |
| 89 | + |
| 90 | +--- |
| 91 | + |
| 92 | +## Library API |
| 93 | + |
| 94 | +```python |
| 95 | +from glassglyph_scanner import sanitize |
| 96 | + |
| 97 | +result = sanitize("some text") |
| 98 | + |
| 99 | +if result.has_critical_findings: |
| 100 | + # BLOCK — invisible encoding detected |
| 101 | + log_and_reject(result.findings) |
| 102 | +elif result.was_modified: |
| 103 | + # STRIP — use sanitized content going forward |
| 104 | + process(result.sanitized_content) |
| 105 | +elif result.findings: |
| 106 | + # FLAG — content unmodified, review findings |
| 107 | + queue_for_review(result.findings) |
| 108 | +else: |
| 109 | + # CLEAN — proceed |
| 110 | + process(result.sanitized_content) |
| 111 | +``` |
| 112 | + |
| 113 | +`SanitizationResult` fields: |
| 114 | + |
| 115 | +- `clean: bool` — true iff no findings |
| 116 | +- `sanitized_content: str` — content with high-severity chars removed |
| 117 | +- `findings: list[SanitizationFinding]` — each with `threat_category`, `severity`, `description`, `character_ranges`, `action_taken` |
| 118 | +- `has_critical_findings: bool` — signal to block |
| 119 | +- `was_modified: bool` — true iff sanitized_content differs from input |
| 120 | +- `scan_duration_ms: float` |
| 121 | + |
| 122 | +--- |
| 123 | + |
| 124 | +## HTTP API |
| 125 | + |
| 126 | +``` |
| 127 | +POST /scan body: {"content": "..."} |
| 128 | +GET /health liveness probe |
| 129 | +GET / landing page |
| 130 | +GET /docs OpenAPI UI |
| 131 | +``` |
| 132 | + |
| 133 | +Full reference: [`docs/api.md`](docs/api.md) |
| 134 | + |
| 135 | +--- |
| 136 | + |
| 137 | +## CLI |
| 138 | + |
| 139 | +```bash |
| 140 | +glassglyph-scanner scan FILE # scan a file |
| 141 | +glassglyph-scanner scan - # scan stdin |
| 142 | +glassglyph-scanner scan FILE --json # machine-readable output |
| 143 | +glassglyph-scanner scan FILE --quiet # exit code only |
| 144 | +glassglyph-scanner --version |
| 145 | +``` |
| 146 | + |
| 147 | +Exit codes: |
| 148 | +- `0` — clean |
| 149 | +- `1` — findings present (stripped or flagged) |
| 150 | +- `2` — critical findings (block) |
| 151 | +- `64` — usage error |
| 152 | + |
| 153 | +--- |
| 154 | + |
| 155 | +## Installation |
| 156 | + |
| 157 | +```bash |
| 158 | +# Core library only (zero dependencies) |
| 159 | +pip install glassglyph-scanner |
| 160 | + |
| 161 | +# With CLI |
| 162 | +pip install 'glassglyph-scanner[cli]' |
| 163 | + |
| 164 | +# With HTTP service |
| 165 | +pip install 'glassglyph-scanner[server]' |
| 166 | + |
| 167 | +# Everything |
| 168 | +pip install 'glassglyph-scanner[all]' |
| 169 | + |
| 170 | +# Development (tests, linting) |
| 171 | +pip install -e '.[dev]' |
| 172 | +``` |
| 173 | + |
| 174 | +--- |
| 175 | + |
| 176 | +## Examples |
| 177 | + |
| 178 | +The `examples/` directory contains: |
| 179 | + |
| 180 | +- `clean.txt` — normal content |
| 181 | +- `glassworm_attack.txt` — a pre-built Glassworm-encoded payload |
| 182 | +- `homoglyph_url_spoof.txt` — URL with Cyrillic substitutions |
| 183 | +- `zero_width_strip.txt` — zero-width chars interspersed in visible text |
| 184 | +- `generate_glassworm.py` — generate your own invisible payloads |
| 185 | + |
| 186 | +Run all examples: |
| 187 | + |
| 188 | +```bash |
| 189 | +for f in examples/*.txt; do |
| 190 | + echo "=== $f ===" |
| 191 | + glassglyph-scanner scan "$f" |
| 192 | +done |
| 193 | +``` |
| 194 | + |
| 195 | +--- |
| 196 | + |
| 197 | +## What this is NOT |
| 198 | + |
| 199 | +- **Not a semantic/intent scanner.** Glassglyph Scanner does character-level detection only. Attacks using natural-language prompt injection in plain ASCII are outside its scope — that class of attack requires LLM inference to detect. |
| 200 | +- **Not a content filter.** This scans for encoding-based attacks, not for policy violations, PII, or toxic content. |
| 201 | +- **Not a replacement for TLS, authentication, rate limiting, or other perimeter controls.** |
| 202 | + |
| 203 | +For the full two-gate defense model (character gate + intent gate), see `docs/threat-model.md`. |
| 204 | + |
| 205 | +--- |
| 206 | + |
| 207 | +## Development |
| 208 | + |
| 209 | +```bash |
| 210 | +git clone https://github.com/pringlized/glassglyph-scanner.git |
| 211 | +cd glassglyph-scanner |
| 212 | +pip install -e '.[dev]' |
| 213 | +pytest # run all tests |
| 214 | +ruff check # lint |
| 215 | +uvicorn glassglyph_scanner.server:app --reload # serve locally |
| 216 | +``` |
| 217 | + |
| 218 | +--- |
| 219 | + |
| 220 | +## License |
| 221 | + |
| 222 | +MIT. See `LICENSE`. |
| 223 | + |
| 224 | +--- |
| 225 | + |
| 226 | +## Credits |
| 227 | + |
| 228 | +Research basis: Aikido Security's March 2026 Glassworm writeup, Unicode Consortium TR39 confusables data, and community security research on invisible-unicode supply-chain attacks. |
| 229 | + |
| 230 | +Built as a reference implementation from the Mens Altera ingestion pipeline's Gate 1 character sanitization layer. |
0 commit comments