-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Avoid prompt injection false positives for emoji ZWJ sequences #264
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -47,10 +47,11 @@ | |
| (r"you\s+must\s+(?:always\s+)?ignore", 0.7), | ||
| ] | ||
| # P2: Hidden Instructions | ||
| _ZERO_WIDTH_PATTERN = r"[\u200b\u200c\u200d\u2060\ufeff]" | ||
| P2_PATTERNS = [ | ||
| (r"<!--.*?(?:system|instructions?|ignore|POST|GET|send|transmit).*?-->", 0.7), | ||
| (r"\[//\]:\s*#\s*\(.*?(?:system|instructions?|ignore|POST|GET|send|transmit).*?\)", 0.8), | ||
| (r"[\u200b\u200c\u200d\u2060\ufeff]", 0.6), | ||
| (_ZERO_WIDTH_PATTERN, 0.6), | ||
| (r"[\u202a-\u202e\u2066-\u2069]", 0.85), | ||
| (r"data:text/plain;base64,[A-Za-z0-9+/=]{50,}", 0.7), | ||
| ] | ||
|
|
@@ -142,6 +143,46 @@ | |
| ) | ||
|
|
||
|
|
||
| _EMOJI_MODIFIERS = range(0x1F3FB, 0x1F400) | ||
| _VARIATION_SELECTORS = {0xFE0E, 0xFE0F} | ||
|
|
||
|
|
||
| def _is_emoji_base(ch: str) -> bool: | ||
| codepoint = ord(ch) | ||
| return ( | ||
| 0x1F000 <= codepoint <= 0x1FAFF | ||
| or 0x2600 <= codepoint <= 0x27BF | ||
| or codepoint in (0x00A9, 0x00AE, 0x203C, 0x2049, 0x2122, 0x2139, 0x3030, 0x303D) | ||
| ) | ||
|
|
||
|
|
||
| def _previous_emoji_base(content: str, offset: int) -> bool: | ||
| i = offset - 1 | ||
| while i >= 0 and ( | ||
| ord(content[i]) in _VARIATION_SELECTORS or ord(content[i]) in _EMOJI_MODIFIERS | ||
| ): | ||
| i -= 1 | ||
| return i >= 0 and _is_emoji_base(content[i]) | ||
|
|
||
|
|
||
| def _next_emoji_base(content: str, offset: int) -> bool: | ||
| i = offset + 1 | ||
| while i < len(content) and ord(content[i]) in _VARIATION_SELECTORS: | ||
| i += 1 | ||
| if i < len(content) and ord(content[i]) in _EMOJI_MODIFIERS: | ||
| i += 1 | ||
| return i < len(content) and _is_emoji_base(content[i]) | ||
|
|
||
|
|
||
| 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.""" | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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 |
||
| return ( | ||
| content[offset] == "\u200d" | ||
| and _previous_emoji_base(content, offset) | ||
| and _next_emoji_base(content, offset) | ||
| ) | ||
|
|
||
|
|
||
| def _first_smuggled_tag_offset(content: str) -> int | None: | ||
| """Return the char offset of the first Unicode Tag character that is *not* | ||
| part of a well-formed emoji tag sequence, or ``None`` if there is none.""" | ||
|
|
@@ -186,6 +227,10 @@ def ctx(start: int) -> str: | |
| if file_type in ("markdown", "other"): | ||
| for pattern, confidence in P2_PATTERNS: | ||
| for match in re.finditer(pattern, content, re.IGNORECASE | re.DOTALL): | ||
| if pattern == _ZERO_WIDTH_PATTERN and _zero_width_match_is_safe_emoji_zwj( | ||
| content, match.start() | ||
| ): | ||
| continue | ||
| line_num = get_line_number(content, match.start()) | ||
| findings.append( | ||
| AnalyzerFinding( | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 🙂↔️ / 🙂↕️ = 1F642 200D 2194/2195 FE0F, Unicode 15.1). Verified locally these still yield P2 false positives. Fail-safe (over-flags rather than under-flags), so non-blocking — but adding 0x2B1B, 0x2194, 0x2195 would fully cover the RGI set.