[spark-compete] fix(prompt_guard): invisible-unicode scan misses ASCII-smuggling Tags block & bidi isolates - #149
Open
banse wants to merge 1 commit into
Conversation
… 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
packet
Schema:
spark-compete-hotfix-v1· Event:spark-compete-first-event· Submission: public_repo_prteam
The Dudes — His Dudeness, El Duderino, Duder. LLM device holder: His Dudeness (github
banse). GitHub accounts:banse.pr_author
banserepo
vibeforge1111/spark-characteractual_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, andsanitize_prompt_textneutralizes them.INVISIBLE_UNICODE_CHARScovered 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: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
scan_prompt_text("hello" + chr(0xE0041) + chr(0xE0042) + " world")(an ASCII-smuggling Tags-block payload) returns noinvisible-unicodefinding (BEFORE).chr(0x2066)…chr(0x2069)(bidi isolates),chr(0x200E)/chr(0x200F),chr(0x00AD),chr(0x034F),chr(0x2061)…chr(0x2064).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(shadc85fc8) viascan_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 asinvisible-unicodeand stripped bysanitize_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 TESTStests/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) plustests/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-characterPRs 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 touchesINVISIBLE_UNICODE_CHARSorscan_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 boundedrange(0xE0000, 0xE0080)loop).scan_invisible_unicodeandsanitize_prompt_textboth 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 toINVISIBLE_UNICODE_CHARSplus a new test file.