Avoid prompt injection false positives for emoji ZWJ sequences#264
Conversation
Signed-off-by: kigland <shuaizhicheng336@gmail.com>
rng1995
left a comment
There was a problem hiding this comment.
[Automated SkillSpector Review]
Summary: Fixes #259 — the P2 zero-width-character rule flagged the U+200D ZWJ inside legitimate emoji ZWJ sequences (e.g. 🧑⚖️). The PR names the zero-width pattern (_ZERO_WIDTH_PATTERN), and in the P2 loop skips a match only when it is a ZWJ whose immediate neighbors (after skipping variation selectors / skin-tone modifiers) are both emoji-base codepoints. Tests added at both the unit (tests/unit/test_patterns.py) and runner (tests/nodes/analyzers/test_static_patterns.py) levels.
Security verification (this is the key risk for an FP-reduction change in a detection rule): I applied the patch locally and probed the carve-out adversarially. All of the following still produce P2 findings: bare ZWJ in ASCII text (text\u200dSYSTEM), ZWJ with an emoji on only one side, doubled ZWJ between emoji, ZWJ at start/end of content, keyword-splitting (ignore\u200dall), and every other zero-width codepoint (U+200B/U+200C/U+2060/U+FEFF) — including between two emoji. The exemption is limited to a single U+200D flanked by visible emoji bases, which hides no text, so no detection gap is introduced. Bidi/Trojan-Source and Unicode Tag smuggling paths are untouched. All 89 tests in the two affected files pass.
Non-blocking nits:
_is_emoji_basemisses a few codepoints used in RGI ZWJ sequences: U+2B1B (⬛ — black cat 🐈⬛, blackbird 🐦⬛) and U+2194/U+2195 (head-shaking 🙂↔️ / 🙂↕️ , Unicode 15.1). Verified locally that these still produce P2 false positives. The miss is fail-safe (over-flagging, not under-flagging), so not a blocker, but worth extending the set.- The neighboring
_EMOJI_TAG_SEQUENCEcarve-out documents in detail why it is safe/narrow; a short comment on_is_emoji_baseexplaining why the broad 0x1F000–0x1FAFF range is acceptable (ZWJ between visible symbols conveys no hidden text) would match that repo convention. - Minor asymmetry:
_previous_emoji_baseskips any run of VS+modifiers, while_next_emoji_baseskips VS then at most one modifier. Harmless for well-formed sequences (base always directly follows ZWJ), but unifying would simplify. - Residual theoretical channel: presence/absence of ZWJ between consecutive emoji could encode steganographic bits undetected. Such bits are not LLM-readable instructions, so the risk is negligible for the prompt-injection threat model; noting for completeness only.
Decision: Approve. Correct, well-tested, self-contained (stdlib-only), fail-safe carve-out; no schema changes.
Note for maintainers: PR #260 fixes the same issue #259 with an equivalent carve-out in the same hunk — the two PRs conflict and only one should land.
| return ( | ||
| 0x1F000 <= codepoint <= 0x1FAFF | ||
| or 0x2600 <= codepoint <= 0x27BF | ||
| or codepoint in (0x00A9, 0x00AE, 0x203C, 0x2049, 0x2122, 0x2139, 0x3030, 0x303D) |
There was a problem hiding this comment.
This set misses a few codepoints that appear in RGI emoji ZWJ sequences: U+2B1B (black cat 🐈⬛ = 1F408 200D 2B1B, blackbird 🐦⬛) and U+2194/U+2195 (head-shaking 🙂
|
|
||
|
|
||
| def _zero_width_match_is_safe_emoji_zwj(content: str, offset: int) -> bool: | ||
| """Allow ZWJ only when it joins two emoji bases in an emoji sequence.""" |
There was a problem hiding this comment.
Suggest documenting why this carve-out is safe despite not validating against the RGI sequence list (a lone ZWJ between two visible emoji bases cannot hide readable text; all other zero-width chars and one-sided/doubled ZWJs remain flagged) — mirroring the detailed narrowness rationale on _EMOJI_TAG_SEQUENCE above.
Problem
Fixes #259. The static P2 hidden-instruction rule currently flags every U+200D zero-width joiner, including normal visible emoji ZWJ sequences such as
🧑⚖️. That makes benign skill text look like prompt injection.Fix
Allow U+200D only when it joins two emoji bases in an emoji sequence. Other zero-width characters, and bare ZWJ usage such as
text\u200dSYSTEM, still produce P2 findings.Tests
.venv/bin/python -m pytest tests/nodes/analyzers/test_static_patterns.py tests/unit/test_patterns.py.venv/bin/ruff check src/skillspector/nodes/analyzers/static_patterns_prompt_injection.py tests/nodes/analyzers/test_static_patterns.py tests/unit/test_patterns.py.venv/bin/ruff format --check src/skillspector/nodes/analyzers/static_patterns_prompt_injection.py tests/nodes/analyzers/test_static_patterns.py tests/unit/test_patterns.pygit diff --checkRisk
Low. The carve-out is limited to ZWJ between emoji-like codepoints; bare ZWJ and non-ZWJ hidden characters remain flagged.