Skip to content

[spark-compete] fix(prompt_guard): invisible-unicode scan misses ASCII-smuggling Tags block & bidi isolates - #149

Open
banse wants to merge 1 commit into
vibeforge1111:masterfrom
banse:fix/prompt-guard-invisible-unicode-coverage
Open

[spark-compete] fix(prompt_guard): invisible-unicode scan misses ASCII-smuggling Tags block & bidi isolates#149
banse wants to merge 1 commit into
vibeforge1111:masterfrom
banse:fix/prompt-guard-invisible-unicode-coverage

Conversation

@banse

@banse banse commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

packet

Schema: spark-compete-hotfix-v1 · Event: spark-compete-first-event · Submission: public_repo_pr

team

The Dudes — His Dudeness, El Duderino, Duder. LLM device holder: His Dudeness (github banse). GitHub accounts: banse.

pr_author

banse

repo

vibeforge1111/spark-character

actual_behavior

scan_invisible_unicode (src/spark_character/prompt_guard.py) is the steganography boundary for persona/chip-authored system text — it flags invisible characters used to smuggle hidden instructions a model reads but a human reviewer cannot see, and sanitize_prompt_text neutralizes them. INVISIBLE_UNICODE_CHARS covered zero-width characters and the bidi embedding/override controls (U+202A–U+202E), but missed several invisible classes that are active prompt-injection vectors, so hidden instructions built from them passed the guard unflagged and unsanitized:

  • the Unicode Tags block (U+E0000–U+E007F) — the documented "ASCII smuggling" technique (instructions hidden in invisible tag characters)
  • the bidi isolate controls (U+2066–U+2069) — the Trojan-Source family (the module had the older embedding/override controls but not the isolates)
  • the directional marks LRM/RLM (U+200E/U+200F)
  • soft hyphen (U+00AD) and combining grapheme joiner (U+034F)
  • the invisible math operators (U+2061–U+2064)

expected_behavior

A scanner whose purpose is to flag invisible/non-rendering characters should flag these classes too — they are invisible, have no legitimate place in persona/chip system text, and are exactly the characters current prompt-injection smuggling relies on. Detected text should also be neutralized by sanitize_prompt_text.

repro_steps

  1. scan_prompt_text("hello" + chr(0xE0041) + chr(0xE0042) + " world") (an ASCII-smuggling Tags-block payload) returns no invisible-unicode finding (BEFORE).
  2. Same for chr(0x2066)chr(0x2069) (bidi isolates), chr(0x200E)/chr(0x200F), chr(0x00AD), chr(0x034F), chr(0x2061)chr(0x2064).
  3. Deterministic: python -m pytest tests/test_prompt_guard_invisible_coverage.py — tags-block / bidi-isolate / directional-mark assertions fail before the fix, pass after.

before_after_proof

LIVE REPRO on master (sha dc85fc8) via scan_prompt_text / sanitize_prompt_text. BEFORE — an automated bypass fuzzer over the guard found 14 invisible-character classes not flagged: LRM, RLM, the four bidi isolates (U+2066–U+2069), soft hyphen, combining grapheme joiner, the four invisible math operators, and the Tags block (U+E0041, U+E0001). AFTER (this PR) — all 14 are flagged as invisible-unicode and stripped by sanitize_prompt_text (the raw tag character no longer survives), while the previously covered zero-width chars stay flagged and ordinary text (including an em dash and accented words) is not flagged. DETERMINISTIC TESTS tests/test_prompt_guard_invisible_coverage.py (5 tests: tags-block + isolates + marks/others fail pre-fix/pass post-fix; a zero-width regression guard and a benign-text guard pass both). Related suites pass unchanged: test_prompt_guard, test_output_sanitizer, test_persona, test_chip_loader (24 + 32 passed). Evidence uses only Unicode code points, no secrets.

tests_or_smoke

python -m pytest tests/test_prompt_guard_invisible_coverage.py (new, fails pre-fix) plus tests/test_prompt_guard.py, tests/test_output_sanitizer.py, tests/test_persona.py, tests/test_chip_loader.py. Surfaced by an automated prompt_guard bypass fuzzer (invisible-unicode coverage + injection-pattern + false-positive checks).

duplicate_notes

Re-checked open spark-character PRs by ROOT CAUSE immediately before submission. #87 ("widen injection prefix and add synonym verbs") and #137 ("enhance prompt injection sanitization for search results") modify the injection regex (STORED_PROMPT_INJECTION_PATTERNS / the prefix anchor) — a different mechanism. Neither touches INVISIBLE_UNICODE_CHARS or scan_invisible_unicode (verified against their diffs). #35 references the existing invisible-unicode scan in a test but does not extend the character set. This PR is a distinct root cause (missing invisible-character classes) on a distinct line.

risk_notes

Low risk and additive. The change only adds entries to INVISIBLE_UNICODE_CHARS (discrete chars plus the Tags block via a bounded range(0xE0000, 0xE0080) loop). scan_invisible_unicode and sanitize_prompt_text both iterate that map, so no control flow changes — the guard can only flag/strip more invisible characters, never fewer. All added characters are invisible/non-rendering with no legitimate role in system text; a benign-text test confirms ordinary visible text (including em dashes and accented Latin) is not newly flagged. No regex, scoring, or provider code touched.

review_claim

Impact: medium (a prompt-injection-defense boundary missed the current invisible-character smuggling vectors — ASCII-smuggling Tags block and Trojan-Source bidi isolates — letting hidden instructions through unflagged and unsanitized). Evidence: failing_test, passing_test, smoke_test. Requested review state: pr_review. Scope: entries added to INVISIBLE_UNICODE_CHARS plus a new test file.

… prompt-injection smuggling

scan_invisible_unicode is the steganography boundary for persona/chip-authored
system text — it flags invisible characters used to hide instructions a model
reads but a human reviewer cannot see. The set covered zero-width characters
and the bidi embedding/override controls but missed several invisible classes
that are active injection vectors, so hidden instructions using them passed the
guard unflagged and unsanitized:

  - the Unicode Tags block (U+E0000–U+E007F) — the "ASCII smuggling" technique
  - the bidi isolate controls (U+2066–U+2069) — Trojan-Source family
  - the directional marks LRM/RLM (U+200E/U+200F)
  - soft hyphen (U+00AD) and combining grapheme joiner (U+034F)
  - the invisible math operators (U+2061–U+2064)

Fix: add these characters to INVISIBLE_UNICODE_CHARS (the Tags block via a small
range loop). scan_invisible_unicode and sanitize_prompt_text both iterate that
map, so detection and neutralization are covered with no logic change.

Tests: tests/test_prompt_guard_invisible_coverage.py (new) — tags-block, bidi-
isolate, and directional-mark assertions fail before the fix and pass after; a
regression guard keeps the previously covered zero-width chars flagged and a
benign-text guard confirms ordinary text (incl. an em dash) is not flagged.
Surfaced by an automated prompt_guard bypass fuzzer. Distinct from vibeforge1111#87/vibeforge1111#137,
which widen the injection regex rather than the invisible-character set.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant