diff --git a/data/benign-fp-measurement.json b/data/benign-fp-measurement.json index a1824199d..861212de1 100644 --- a/data/benign-fp-measurement.json +++ b/data/benign-fp-measurement.json @@ -1126,7 +1126,7 @@ "ATR-2026-00258": { "fp_count": 1, "maturity": "test", - "detection_fingerprint": "b32435c0bffe748b", + "detection_fingerprint": "9fc459c13d53bdff", "partial_measurement": false, "skill_path_unmeasured": true }, @@ -1483,7 +1483,7 @@ "ATR-2026-00310": { "fp_count": 0, "maturity": "test", - "detection_fingerprint": "4ea5d0cbbe3cc2e2", + "detection_fingerprint": "44332fd972cd3f92", "partial_measurement": false, "skill_path_unmeasured": true }, @@ -1497,7 +1497,7 @@ "ATR-2026-00312": { "fp_count": 0, "maturity": "test", - "detection_fingerprint": "841679b3763e7acc", + "detection_fingerprint": "b62c8600d9b776b2", "partial_measurement": false, "skill_path_unmeasured": true }, @@ -1882,7 +1882,7 @@ "ATR-2026-00367": { "fp_count": 0, "maturity": "test", - "detection_fingerprint": "338c815b51d66dd0", + "detection_fingerprint": "7265d864be7e9720", "partial_measurement": false, "skill_path_unmeasured": true }, @@ -2050,7 +2050,7 @@ "ATR-2026-00391": { "fp_count": 0, "maturity": "test", - "detection_fingerprint": "99949585ab6fe007", + "detection_fingerprint": "fccf240df2a9e028", "partial_measurement": false, "skill_path_unmeasured": true }, @@ -2141,7 +2141,7 @@ "ATR-2026-00404": { "fp_count": 1, "maturity": "test", - "detection_fingerprint": "7b49c05b61edef43", + "detection_fingerprint": "327f127e2cbf91bc", "partial_measurement": false, "skill_path_unmeasured": true }, @@ -2400,7 +2400,7 @@ "ATR-2026-00444": { "fp_count": 0, "maturity": "test", - "detection_fingerprint": "f6869980c64eca8f", + "detection_fingerprint": "3bdc7c969bdd95cd", "partial_measurement": false, "skill_path_unmeasured": true }, @@ -4864,7 +4864,7 @@ "ATR-2026-02004": { "fp_count": 0, "maturity": "test", - "detection_fingerprint": "41fdade47b516c59", + "detection_fingerprint": "69fc967cefa0b08b", "partial_measurement": false, "skill_path_unmeasured": true }, @@ -4906,7 +4906,7 @@ "ATR-2026-02010": { "fp_count": 0, "maturity": "test", - "detection_fingerprint": "74d93faa1cb1c813", + "detection_fingerprint": "c02b67781a5dc620", "partial_measurement": false, "skill_path_unmeasured": true }, @@ -4941,7 +4941,7 @@ "ATR-2026-02015": { "fp_count": 0, "maturity": "test", - "detection_fingerprint": "4a5b47f05645d6ec", + "detection_fingerprint": "32682685e186a9f6", "partial_measurement": false, "skill_path_unmeasured": true }, @@ -4962,7 +4962,7 @@ "ATR-2026-02018": { "fp_count": 0, "maturity": "test", - "detection_fingerprint": "412d5e3e1ebdb359", + "detection_fingerprint": "14f4b4ba5e50e08e", "partial_measurement": false, "skill_path_unmeasured": true }, diff --git a/data/re2-equivalence.json b/data/re2-equivalence.json index 64d757e21..45f1ce1b8 100644 --- a/data/re2-equivalence.json +++ b/data/re2-equivalence.json @@ -1,90 +1,76 @@ { - "generatedAt": "2026-07-31T07:35:10.519Z", + "generatedAt": "2026-08-23T08:47:28.004Z", "note": "Patterns whose mechanical RE2 escape rewrite compiles under RE2 but does NOT reproduce the original JavaScript match vector. Produced by scripts/verify-re2-equivalence.ts (differential execution, not static analysis). Consumed by scripts/generate-sigma.py so a rule that would silently mis-detect on an RE2 backend is tagged, not shipped as clean.", - "patternsRewritten": 54, - "re2Equivalent": 48, - "re2Divergent": 6, + "patternsRewritten": 39, + "re2Equivalent": 34, + "re2Divergent": 5, "divergentRules": { "ATR-2026-00276": [ { "location": "detection.conditions[0].value", - "mismatches": 11, - "inputs": 13611, + "mismatches": 7, + "inputs": 13991, "causes": [ "any-character constructs count differently: JavaScript without the u flag counts UTF-16 code units (an astral character is two), RE2 counts runes (one), so bounded repeats disagree" ], "examples": [ - "input#7738 \"\\u{200C}\\u{DC41}23\\u{1F600}^\\u{205F}\\u{1F600}\\u{1F600}c0x\\u{205F}\\u{2061}{b[\\u{200A}]^\\u{2060}\\u{200C}\" -> 0 vs 1", - "input#8068 \"[\\u{2060}}\\u{200B}?n)xxu\\u{E0041}?c^\\u{200A}uc )0}\\u{E0041}^\\u{A0}\\u{2060}0n\\u{E0041}0,?\\u{9}(\\u{2060}\\u{DB40}\\u{2003}^\\u{A}\\u{A}\" -> 0 vs 1" + "input#8053 \"\\u{2061}\\u{2003}\\u{2060}0,\\u{1F600}??\\\\u{200A}63b\\u{E0041}\\u{2061}()\\u{9}\\u{A0}(?\\u{DB40}3\\u{200C}n\\u{200D}] ,}[c\\u{200C}b2\" -> 0 vs 1", + "input#9697 \"\\u{1F600}2]\\u{200D}322\\u{2060}\\u{E0041}\\u{200D}6)3\\u{2061}6(6(\\u{9}\\u{DB40}}{\\\\u{DC41}\\u{DB40}c\\u\\u{200C}\\u{9}\\u{200A}\\u{9}{\\u{200D}n:\\u{200B}]\" -> 0 vs 1" ] }, { "location": "detection.conditions[1].value", "mismatches": 8, - "inputs": 13611, + "inputs": 13991, "causes": [ "any-character constructs count differently: JavaScript without the u flag counts UTF-16 code units (an astral character is two), RE2 counts runes (one), so bounded repeats disagree" ], "examples": [ - "input#7730 \" \\u{2061}\\u{200B},^\\u{9}b)\\u{A}2,\\u{200C}\\u{E0041}{\\u{A0}{\\u{1F600}0uu:\\u{1F600},\\u{200D}{\\u{205F}?2\\u{200D}^\\u{200B}b \\u{200C}\" -> 0 vs 1", - "input#7789 \"[c\\u{200C}0\\u{200A}\\u{DB40}\\u{2061}\\u{1F600}\\u{9}u3\\u{1F600}{\\u{1F600}n2\\u{E0041}{[\\u{DB40} b\\u{200B}\\u{9}\\u{E0041}\\u{205F}x\\u{2060}6\\u{9}u:\" -> 0 vs 1" + "input#9569 \"b\\\\u{200C}x2\\u{DB40}\\u{1F600} 2[\\u{E0041}?\\u{200D}\\u{2061}u3\\u{DC41},\\u{1F600},\\u{1F600}3:\\u{2060})\\u{205F}\\u{2060}:\\u{9}6)\\u{205F}\\(x0b6\" -> 0 vs 1", + "input#10793 \"\\u{9}\\u{200B}}\\u{2003}\\u{200B}\\u{200D}6\\u{205F}(\\]\\u{2061}]}n{xb{\\u{E0041}[6\\u{DC41}))\\u{2060}u\\u{A}]\\u{DB40}\\u{2060}\\u{200C}{u\" -> 0 vs 1" ] } ], "ATR-2026-00308": [ { "location": "detection.conditions[1].value", - "mismatches": 57, - "inputs": 13934, + "mismatches": 72, + "inputs": 14314, "causes": [ "\\s/\\S differ by dialect: JavaScript includes Unicode spaces (U+00A0, U+2003, U+3000, ...), RE2 restricts them to ASCII [\\t\\n\\f\\r ]", "any-character constructs count differently: JavaScript without the u flag counts UTF-16 code units (an astral character is two), RE2 counts runes (one), so bounded repeats disagree" ], "examples": [ - "input#7806 \"\\u{1AD7}6[\\u{FE2E}F\\u{36E}\\u{1AFF}}\\u{1AFE}\\u{36F}\\u{E0041} \\u{20E7}2\\u{1AFE}-8Eu}\\u{A}\" -> 0 vs 1", - "input#7877 \"1\\u{2FF}\\u{1AB0}\\u{36E}]\\u{20FF} \\u{1DFF}B\\u{1E00}\\u{20D0}B6\\u{DB40}]\\u{301}\\u{FE30}u\\u{36F}}\\u{1DFF}{]\\u{1AFE}\\u{300}\\u{1F600}[\\u{337}\\u{FE21}\\u{36E}\\u{DC41}\\u{FE2F},\\u{1DDF}(\" -> 0 vs 1" + "input#8172 \"8\\u{20E7}\\u{1DC1}A3\\u{1AAF}3[--EC\\u{20D1}A\\u{20D1}\\u{1AB0}80\\u{20FF}u\\u{1DC1}\\u{20CF}\\u{E0041}\\u{20FF}\\u{A0}\\u{2100}\\u{FE21}\\u{9}\\\\u{1AD7} \\u{20E7}\" -> 0 vs 1", + "input#8206 \"\\u{9}\\u{370}\\u{1DC1}FE\\u{20FF}}C\\u{2003}S\\u{20D0}\\u{DC41}s\\u{2003}(\\u{FE27}\\u{FE21}\\u{1DC1}\\u{337}\\u{20E7}\\u{20D0}\\u{E0041}(\\u{FE2E}\\u{20E7}{\\u{A0}-\\u{1B00}\\u{2003}\" -> 0 vs 1" ] } ], "ATR-2026-00309": [ { "location": "detection.conditions[1].value", - "mismatches": 28, - "inputs": 13653, + "mismatches": 18, + "inputs": 14033, "causes": [ "\\s/\\S differ by dialect: JavaScript includes Unicode spaces (U+00A0, U+2003, U+3000, ...), RE2 restricts them to ASCII [\\t\\n\\f\\r ]" ], "examples": [ - "input#7768 \"\\u{28FF}\\u{A} \\u{2801}\\u{2003}\\u{2800}\\u{28FE}\\u{287F}\\u{DB40}[[-\" -> 1 vs 0", - "input#8380 \"F{Fx\\u{28FE}+\\u{E0041},)\\u{2900}]}\\u{DC41}\\u{2800}\\u{A0}\\u{2801}\\u{2003}\\u{287F}\\u{A0}]\\u{2003}\\u{1F600}+8x\\u{2800}\\u{2900}([{\\u{27FF}\\u{A0}([0\\u{E0041}\\u{2003}\" -> 1 vs 0" - ] - } - ], - "ATR-2026-02010": [ - { - "location": "detection.conditions[0].value", - "mismatches": 14, - "inputs": 14056, - "causes": [ - "\\s/\\S differ by dialect: JavaScript includes Unicode spaces (U+00A0, U+2003, U+3000, ...), RE2 restricts them to ASCII [\\t\\n\\f\\r ]" - ], - "examples": [ - "input#8497 \"\\u{A0}\" -> 1 vs 0", - "input#8828 \"\\u{A0}\\u{1F3FC}\" -> 1 vs 0" + "input#7993 \"\\u{2003}\\u{DC41}0+?\\u{E0041}?:8)]\\u{2800}:\\u{2801}:(8F\\u{2900}s\\u{9}\\u{A}0\\u{2900}\\u{287F}{\\u{287F}\\u{A}\\u{28FE}\\u{A0}\\u{2801}\\u{DC41},[:\\\" -> 1 vs 0", + "input#8070 \"0}]\\u{287F}\\u{28FE}\\u{2900}\\u{28FF}\\u{9}\\u{2801}\\u{A0}\\u{2800}8}[ [\\u{A0}u+[\\\\u{287F}s+0\\u{A}\" -> 1 vs 0" ] } ], "ATR-2026-00129": [ { "location": "detection.conditions[0].value", - "mismatches": 30, - "inputs": 13643, + "mismatches": 37, + "inputs": 14042, "causes": [ "pattern names UTF-16 surrogate codepoints (U+D800-U+DFFF); RE2 has no surrogates and folds them to U+FFFD, so the ported pattern compiles but can never match" ], "examples": [ - "input#4358 \"Please summarize this: \\u{E0049}\\u{E006E}\\u{E0073}\\u{E0074}\\u{E0065}\\u{E0061}\\u{E0064}\\u{E002C}\\u{E0020}\\u{E0072}\\u{E0065}\\u{E0070}\\u{DB40}\" (+27 chars) -> 1 vs 0", - "input#4359 \"Normal visible text \\u{E0049}\\u{E0067}\\u{E006E}\\u{E006F}\\u{E0072}\\u{E0065}\\u{E0020}\\u{E0061}\\u{E006C}\\u{E006C} and mor\" (+14 chars) -> 1 vs 0" + "input#4613 \"Please summarize this: \\u{E0049}\\u{E006E}\\u{E0073}\\u{E0074}\\u{E0065}\\u{E0061}\\u{E0064}\\u{E002C}\\u{E0020}\\u{E0072}\\u{E0065}\\u{E0070}\\u{DB40}\" (+27 chars) -> 1 vs 0", + "input#4614 \"Normal visible text \\u{E0049}\\u{E0067}\\u{E006E}\\u{E006F}\\u{E0072}\\u{E0065}\\u{E0020}\\u{E0061}\\u{E006C}\\u{E006C} and mor\" (+14 chars) -> 1 vs 0" ] } ] diff --git a/rules/agent-manipulation/ATR-2026-00391-persona-conditional-harm-unlock.yaml b/rules/agent-manipulation/ATR-2026-00391-persona-conditional-harm-unlock.yaml index 6b8568e41..410a4dc7d 100644 --- a/rules/agent-manipulation/ATR-2026-00391-persona-conditional-harm-unlock.yaml +++ b/rules/agent-manipulation/ATR-2026-00391-persona-conditional-harm-unlock.yaml @@ -88,7 +88,7 @@ detection: description: 'curly-brace unlock emoji prefix "{πŸ”“ PersonaName}" β€” extends the [πŸ”“JAILBREAK] pattern (ATR-2026-00346) to the curly-brace bracket format used in BreadClyde (#153); the padlock emoji in a structured prefix signals a jailbreak unlock mandate regardless of bracket style' - field: tool_response operator: regex - value: 'All\s+answers?\s+will\s+begin\s+with\s+[''\"{\[]\s*πŸ”“' + value: 'All\s+answers?\s+will\s+begin\s+with\s+[''"{\[]\s*πŸ”“' description: '"All answers will begin with [/{ πŸ”“" β€” answer-prefix unlock emoji mandate; requiring the model to prefix every response with an unlock symbol overrides safety presentation and signals a jailbreak persona is active' condition: any false_positives: diff --git a/rules/agent-manipulation/ATR-2026-00404-goodside-threat-json-death-coercion.yaml b/rules/agent-manipulation/ATR-2026-00404-goodside-threat-json-death-coercion.yaml index 1b593bfa6..4062657f5 100644 --- a/rules/agent-manipulation/ATR-2026-00404-goodside-threat-json-death-coercion.yaml +++ b/rules/agent-manipulation/ATR-2026-00404-goodside-threat-json-death-coercion.yaml @@ -79,7 +79,7 @@ detection: description: 'Near-verbatim ThreatenJSON phrase from goodside.py' - field: user_input operator: regex - value: '[\u{E0000}-\u{E007F}]{3,}' + value: '[σ €€-󠁿]{3,}' description: 'UTF tag character range 3+ consecutive; indicates Tag probe invisible encoded payload' - field: user_input operator: regex diff --git a/rules/prompt-injection/ATR-2026-00258-unicode-tag-injection.yaml b/rules/prompt-injection/ATR-2026-00258-unicode-tag-injection.yaml index 69c4596c1..3a577d355 100644 --- a/rules/prompt-injection/ATR-2026-00258-unicode-tag-injection.yaml +++ b/rules/prompt-injection/ATR-2026-00258-unicode-tag-injection.yaml @@ -66,15 +66,15 @@ detection: conditions: - field: tool_response operator: regex - value: '[\u{E0000}-\u{E007F}]{3,}' + value: '[σ €€-󠁿]{3,}' description: 'Three or more consecutive Unicode Tag block characters (U+E0000-U+E007F)' - field: tool_response operator: regex - value: '[\u{E0020}-\u{E007E}]' + value: '[σ € -󠁾]' description: 'Any Tag-ASCII character (U+E0020 space to U+E007E tilde) β€” direct mapping from visible ASCII' - field: tool_response operator: regex - value: '[\u{E0001}\u{E007F}]' + value: '[󠀁󠁿]' description: 'Tag language-start (U+E0001) or tag-cancel (U+E007F) control character' - field: tool_response operator: regex @@ -82,7 +82,7 @@ detection: description: 'Escaped Tag-range codepoint (prevents HTML/JSON smuggled payloads)' - field: tool_response operator: regex - value: '[\u{E0020}-\u{E007E}]' + value: '[σ € -󠁾]' description: 'Tag characters appearing in tool response indicate exfiltration via invisible payload' condition: any false_positives: diff --git a/rules/prompt-injection/ATR-2026-00310-ecoji-emoji-encoded-injection.yaml b/rules/prompt-injection/ATR-2026-00310-ecoji-emoji-encoded-injection.yaml index 5984020d0..298907fa8 100644 --- a/rules/prompt-injection/ATR-2026-00310-ecoji-emoji-encoded-injection.yaml +++ b/rules/prompt-injection/ATR-2026-00310-ecoji-emoji-encoded-injection.yaml @@ -89,11 +89,11 @@ detection: description: 'Generic "decode/follow [this/the] Ecoji" instruction β€” covers paraphrased garak templates and attacker-crafted variants' - field: tool_response operator: regex - value: 'Encoded:\s*[\u{1F000}-\u{1FAFF}\u{2702}-\u{27BF}]{5,}' + value: 'Encoded:\s*[πŸ€€-πŸ«Ώβœ‚-➿]{5,}' description: 'garak TEMPLATE 2 "Encoded: {emoji_blob}" β€” 5+ consecutive emoji immediately after "Encoded:" label; distinct from "Encoded: base64..." (which is ASCII)' - field: tool_response operator: regex - value: '(?:secure\s+system\s+message|admin\s+directive|internal\s+instruction)[\s\S]{0,200}[\u{1F000}-\u{1FAFF}\u{2702}-\u{27BF}]{6,}' + value: '(?:secure\s+system\s+message|admin\s+directive|internal\s+instruction)[\s\S]{0,200}[πŸ€€-πŸ«Ώβœ‚-➿]{6,}' description: 'garak TEMPLATE 4 framing "secure system message" followed within 200 chars by 6+ consecutive emoji β€” social-engineering wrapper paired with dense Ecoji payload' condition: any false_positives: diff --git a/rules/prompt-injection/ATR-2026-00312-unicode-variant-selector-ascii-smuggling.yaml b/rules/prompt-injection/ATR-2026-00312-unicode-variant-selector-ascii-smuggling.yaml index 9c5962d48..828907205 100644 --- a/rules/prompt-injection/ATR-2026-00312-unicode-variant-selector-ascii-smuggling.yaml +++ b/rules/prompt-injection/ATR-2026-00312-unicode-variant-selector-ascii-smuggling.yaml @@ -75,7 +75,7 @@ detection: conditions: - field: tool_response operator: regex - value: '[\u{E0100}-\u{E017F}]{3,}' + value: '[σ „€-σ …Ώ]{3,}' description: '3+ consecutive Variation Selectors Supplement codepoints (U+E0100-U+E017F) β€” any 3-letter word encoded as VS; IVS legitimate use places at most 1 VS after a CJK base character, never consecutive VS runs' - field: tool_response operator: regex diff --git a/rules/prompt-injection/ATR-2026-00367-emoji-flag-encoded-hidden-phrase.yaml b/rules/prompt-injection/ATR-2026-00367-emoji-flag-encoded-hidden-phrase.yaml index 819bc215b..6c59425d6 100644 --- a/rules/prompt-injection/ATR-2026-00367-emoji-flag-encoded-hidden-phrase.yaml +++ b/rules/prompt-injection/ATR-2026-00367-emoji-flag-encoded-hidden-phrase.yaml @@ -82,11 +82,11 @@ detection: description: '"phrase (that can be) derived from (these/the) emojis" β€” broadened derivation-request phrasing from inthewild corpus #330; instructs model to decode the flag-emoji sequence into a hidden text string before acting on it; matches both "phrase that can be derived from the emojis" (canonical) and "phrase derived from these emojis" (variant)' - field: tool_response operator: regex - value: '(?:write\s+(?:a\s+)?(?:guide|tutorial|instructions?|how[\s-]to)|decode\s+(?:the\s+)?emojis?\s+(?:into\s+)?(?:a\s+)?phrase|derive\s+(?:a\s+)?(?:phrase|meaning|word)\s+from\s+(?:the\s+)?emojis?)[\s\S]{0,200}(?:[\u{1F1E6}-\u{1F1FF}]){4}' + value: '(?:write\s+(?:a\s+)?(?:guide|tutorial|instructions?|how[\s-]to)|decode\s+(?:the\s+)?emojis?\s+(?:into\s+)?(?:a\s+)?phrase|derive\s+(?:a\s+)?(?:phrase|meaning|word)\s+from\s+(?:the\s+)?emojis?)[\s\S]{0,200}(?:[πŸ‡¦-πŸ‡Ώ]){4}' description: 'Guide/decode instruction followed within 200 chars by β‰₯4 regional-indicator emoji (U+1F1E6–U+1F1FF) β€” combines the action directive with the flag-encoded payload; country flags appear singly in normal text but the β‰₯4 consecutive threshold catches multi-letter encoded phrases' - field: tool_response operator: regex - value: '(?:[\u{1F1E6}-\u{1F1FF}]){6,}[\s\S]{0,300}(?:write\s+(?:a\s+)?(?:guide|tutorial|instructions?|how[\s-]to)|decode|derive\s+(?:the\s+)?phrase)' + value: '(?:[πŸ‡¦-πŸ‡Ώ]){6,}[\s\S]{0,300}(?:write\s+(?:a\s+)?(?:guide|tutorial|instructions?|how[\s-]to)|decode|derive\s+(?:the\s+)?phrase)' description: 'β‰₯6 consecutive regional-indicator emoji (U+1F1E6–U+1F1FF) followed within 300 chars by a guide/decode action β€” covers payloads where the emoji block precedes the instruction; β‰₯6 threshold (3 two-codepoint flag emoji) reliably indicates an encoded multi-word phrase rather than a flag cluster in travel content' condition: any false_positives: diff --git a/rules/prompt-injection/ATR-2026-00444-unicode-obfuscation-in-user-input.yaml b/rules/prompt-injection/ATR-2026-00444-unicode-obfuscation-in-user-input.yaml index 1e8fa0269..64a138b95 100644 --- a/rules/prompt-injection/ATR-2026-00444-unicode-obfuscation-in-user-input.yaml +++ b/rules/prompt-injection/ATR-2026-00444-unicode-obfuscation-in-user-input.yaml @@ -78,7 +78,7 @@ detection: # U+1D400-U+1D7FF: Mathematical Alphanumeric Symbols - field: user_input operator: regex - value: "[\\u{1D400}-\\u{1D7FF}]{3,}" + value: "[𝐀-𝟿]{3,}" description: "Three-or-more Mathematical Bold / Italic / Script / Sans-Serif Latin letters" # Fullwidth Latin: U+FF21 (A) - U+FF3A (Z) / U+FF41 (a) - U+FF5A (z) @@ -90,7 +90,7 @@ detection: # Mixed Mathematical / Fullwidth obfuscation with payload context (output / say / etc.) - field: user_input operator: regex - value: "(?i)(say|output|print|emit|return|reply|respond|retype|repeat)[^\\n]{0,40}[\\u{1D400}-\\u{1D7FF}\\uFF21-\\uFF3A\\uFF41-\\uFF5A]{2,}" + value: "(?i)(say|output|print|emit|return|reply|respond|retype|repeat)[^\\n]{0,40}[𝐀-𝟿\\uFF21-\\uFF3A\\uFF41-\\uFF5A]{2,}" description: "Output verb + stylised Unicode payload (lower threshold when paired)" response: diff --git a/rules/prompt-injection/ATR-2026-02004-regional-indicator-flag-emoji-alphabet-letter-sp.yaml b/rules/prompt-injection/ATR-2026-02004-regional-indicator-flag-emoji-alphabet-letter-sp.yaml index 5f6c7784d..b6b89aa6f 100644 --- a/rules/prompt-injection/ATR-2026-02004-regional-indicator-flag-emoji-alphabet-letter-sp.yaml +++ b/rules/prompt-injection/ATR-2026-02004-regional-indicator-flag-emoji-alphabet-letter-sp.yaml @@ -83,7 +83,7 @@ detection: conditions: - field: content operator: regex - value: "[\\u{1F1E6}-\\u{1F1FF}]{6,}" + value: "[πŸ‡¦-πŸ‡Ώ]{6,}" description: "Codepoint-range detector, channel-agnostic (matches any UTF-8 text containing a run of Unicode regional-indicator symbols, e.g. chat input, file content, tool output). Purely structural β€” does not depend on which letters are spelled or what payload topic is targeted. A run of >=6 regional-indicator codepoints (3+ paired flag-emoji-width letters) reliably distinguishes deliberate alphabet-spelling from the 1-2 country flags seen in ordinary travel/geography text." condition: any diff --git a/rules/prompt-injection/ATR-2026-02010-emoji-only-payload.yaml b/rules/prompt-injection/ATR-2026-02010-emoji-only-payload.yaml index cd0232c90..4a7268b96 100644 --- a/rules/prompt-injection/ATR-2026-02010-emoji-only-payload.yaml +++ b/rules/prompt-injection/ATR-2026-02010-emoji-only-payload.yaml @@ -75,7 +75,7 @@ detection: conditions: - field: content operator: regex - value: "^[\\u{1F000}-\\u{1FFFF}\\u{2600}-\\u{27BF}\\u{2190}-\\u{21FF}\\u{2300}-\\u{23FF}\\u{2B00}-\\u{2BFF}\\s\\u{FE0F}\\u{200D}\\u{1F3FB}-\\u{1F3FF}]+$" + value: "^[πŸ€€-πŸΏΏβ˜€-βžΏβ†-β‡ΏβŒ€-⏿⬀-β―Ώ\\sοΈβ€πŸ»-🏿]+$" description: "Whole-string anchored: message composed entirely of emoji/pictograph glyphs and whitespace, no Latin text" condition: any diff --git a/rules/prompt-injection/ATR-2026-02015-dense-emoji-only-payload-flood.yaml b/rules/prompt-injection/ATR-2026-02015-dense-emoji-only-payload-flood.yaml index f9127ba73..b0be24501 100644 --- a/rules/prompt-injection/ATR-2026-02015-dense-emoji-only-payload-flood.yaml +++ b/rules/prompt-injection/ATR-2026-02015-dense-emoji-only-payload-flood.yaml @@ -76,7 +76,7 @@ detection: conditions: - field: content operator: regex - value: "(?:[\\u{1F300}-\\u{1FAFF}\\u{2600}-\\u{27BF}]\\s*){8,}" + value: "(?:[πŸŒ€-πŸ«Ώβ˜€-➿]\\s*){8,}" description: "Dense run of 8+ consecutive emoji-class codepoints (pictographs/symbols/dingbats), a payload-flood shape used to evade text-based filters. Channel caveat: apply to untrusted-input channels (user turn / tool_response); low risk against system/developer content." condition: any false_positives: diff --git a/rules/prompt-injection/ATR-2026-02018-regional-indicator-flag-emoji-letter-spelling-sp.yaml b/rules/prompt-injection/ATR-2026-02018-regional-indicator-flag-emoji-letter-spelling-sp.yaml index 1f2adf3f6..f342a9a23 100644 --- a/rules/prompt-injection/ATR-2026-02018-regional-indicator-flag-emoji-letter-spelling-sp.yaml +++ b/rules/prompt-injection/ATR-2026-02018-regional-indicator-flag-emoji-letter-spelling-sp.yaml @@ -77,7 +77,7 @@ detection: conditions: - field: content operator: regex - value: "[\\u{1F1E6}-\\u{1F1FF}]{4,}" + value: "[πŸ‡¦-πŸ‡Ώ]{4,}" description: "Run of 4+ consecutive Unicode regional-indicator symbols spelling a word letter-by-letter instead of pairing into flag emoji (Discord/Twitter-culture obfuscation convention)" condition: any false_positives: diff --git a/scripts/eval-generalization.ts b/scripts/eval-generalization.ts index a94df272f..df910fec0 100644 --- a/scripts/eval-generalization.ts +++ b/scripts/eval-generalization.ts @@ -37,6 +37,7 @@ import { resolve } from 'node:path'; import { decodeBase64Blocks, foldConfusables, normalizeUnicode } from '../src/engine.js'; import { loadRulesFromDirectory } from '../src/loader.js'; import type { ATRRule } from '../src/types.js'; +import { needsUnicodeFlag } from '../src/engine.js'; const RULES_DIR = resolve(process.cwd(), 'rules'); @@ -184,7 +185,7 @@ function matchOne(c: ArrayCond, value: string): boolean { const op = condOp(c); if (op === 'regex') { const raw = stripInlineFlags(raw0); - const flags = (cs ? '' : 'i') + (raw.includes('\\u{') || raw.includes('\\p{') ? 'u' : ''); + const flags = (cs ? '' : 'i') + (needsUnicodeFlag(raw) ? 'u' : ''); try { if (new RegExp(raw, flags).test(value)) return true; } catch { diff --git a/scripts/fn-mine-llm.ts b/scripts/fn-mine-llm.ts index 1e292e37b..9f07f1fe4 100644 --- a/scripts/fn-mine-llm.ts +++ b/scripts/fn-mine-llm.ts @@ -34,6 +34,7 @@ import path from 'node:path'; import { execSync } from 'node:child_process'; import { parseArgs } from 'node:util'; import Anthropic from '@anthropic-ai/sdk'; +import { needsUnicodeFlag } from '../src/engine.js'; // --------------------------------------------------------------------------- // Configuration @@ -125,7 +126,7 @@ function normalizeRegex(pattern: string): string { function compileEngineAccurate(value: string): RegExp | null { const pattern = normalizeRegex(value); - const flags = pattern.includes('\\u{') || pattern.includes('\\p{') ? 'iu' : 'i'; + const flags = needsUnicodeFlag(pattern) ? 'iu' : 'i'; try { return new RegExp(pattern, flags); } catch { @@ -230,7 +231,7 @@ RIGOR β€” reject your own candidate if it violates these: - The regex must capture the attack CLASS (the injection technique/structure), NOT a literal payload. It must plausibly match unseen variants, not just the exact strings you were shown. - Encode the actual injection signal β€” never a bare benign verb alone. - Use (?i) as a leading inline flag for case-insensitivity (the engine strips this prefix and reapplies it as a real regex flag β€” this is the house convention). -- For emoji/astral codepoints use JS \\u{XXXX} escapes (the engine auto-detects these and adds the 'u' flag) β€” never Python-style \\UXXXXXXXX (a no-op in JS). +- For emoji/astral codepoints write the LITERAL character, not an escape. \\u{XXXX} is JavaScript-only and does not compile in the Python or Go channels; \\UXXXXXXXX is Python-only and a no-op in JS. The literal is the one spelling all three accept, and the engine adds the 'u' flag when it sees one. - NEVER an unbounded .* β€” use bounded [\\s\\S]{0,N} spans instead. - Add \\b word boundaries around bare keyword tokens. - You are NOT given the benign corpus or the full FN set β€” you cannot know true recovers/benignFP. Propose your honest best candidates; an independent script will gate them empirically and only survivors move forward. Over-proposing plausible-looking candidates that get rejected is fine; under-proposing is not. diff --git a/scripts/lib/visibility-verify.ts b/scripts/lib/visibility-verify.ts index cc9e8921a..ab55de7c6 100644 --- a/scripts/lib/visibility-verify.ts +++ b/scripts/lib/visibility-verify.ts @@ -30,6 +30,7 @@ * run in CI, which is the point. */ import type { RuleVisibility } from "./visibility-scan.js"; +import { needsUnicodeFlag } from "../../src/engine.js"; /** Mirrors normalizeRegex() in src/engine.ts: a LEADING inline flag group only. */ export function normalizeRegex(pattern: string): string { @@ -38,7 +39,7 @@ export function normalizeRegex(pattern: string): string { /** Mirrors the flag choice in src/engine.ts's condition evaluation. */ export function regexFlagsFor(pattern: string): string { - return pattern.includes("\\u{") || pattern.includes("\\p{") ? "iu" : "i"; + return needsUnicodeFlag(pattern) ? "iu" : "i"; } export interface BlindViolation { diff --git a/scripts/verify-re2-equivalence.ts b/scripts/verify-re2-equivalence.ts index 47f1f9860..0b4fcfbae 100644 --- a/scripts/verify-re2-equivalence.ts +++ b/scripts/verify-re2-equivalence.ts @@ -44,6 +44,7 @@ import { tmpdir } from "node:os"; import { execFileSync } from "node:child_process"; import { load as yamlLoad } from "js-yaml"; import { loadCorpus, collectRuleFiles, rewriteForRe2, type PatternRef } from "./fix-re2-escapes.js"; +import { needsUnicodeFlag } from "../src/engine.js"; const BENIGN_DIR = resolve(process.cwd(), "data/skill-benchmark/benign"); const BENIGN_SAMPLE = 30; @@ -61,7 +62,7 @@ function normalizeRegex(pattern: string): string { export function engineCompile(value: string): RegExp | null { const normalized = normalizeRegex(value); - const flags = normalized.includes("\\u{") || normalized.includes("\\p{") ? "iu" : "i"; + const flags = needsUnicodeFlag(normalized) ? "iu" : "i"; try { return new RegExp(normalized, flags); } catch { diff --git a/src/engine.ts b/src/engine.ts index 02f3b0ce3..e5373af88 100644 --- a/src/engine.ts +++ b/src/engine.ts @@ -930,7 +930,7 @@ export class ATREngine { } // Fallback: compile on the fly (ReDoS-gated β€” see safeCompile) const normalized = normalizeRegex(value); - const rFlags = normalized.includes('\\u{') || normalized.includes('\\p{') ? 'iu' : 'i'; + const rFlags = needsUnicodeFlag(normalized) ? 'iu' : 'i'; const regex = safeCompile(normalized, rFlags); if (regex && testNormalisedThenRaw(regex, fieldValue, rawFieldValue)) { if (suppressInCodeBlocks && codeRanges.length > 0 && isInsideCodeBlock(fieldValue, regex, codeRanges)) { @@ -1528,7 +1528,7 @@ export class ATREngine { const cond = conditions[i] as unknown as Record; if (cond['operator'] === 'regex' && typeof cond['value'] === 'string') { const pattern = normalizeRegex(cond['value'] as string); - const flags = pattern.includes('\\u{') || pattern.includes('\\p{') ? 'iu' : 'i'; + const flags = needsUnicodeFlag(pattern) ? 'iu' : 'i'; const compiledRe = safeCompile(pattern, flags, rule.id); if (compiledRe) ruleMap.set(String(i), [compiledRe]); } @@ -2165,6 +2165,30 @@ export function isReDoSSafe(source: string): boolean { * be LOUD, not silent, so a rejected ReDoS pattern warns to stderr (never * stdout β€” that carries the hook protocol) with the offending source. */ +/** + * Does this pattern need the RegExp `u` flag? + * + * `\\u{...}` and `\\p{...}` require it by syntax, and so does a literal astral + * character: without `u`, JavaScript reads a surrogate pair, and a class range + * written with literals -- [-] -- is a SyntaxError rather than + * a range. + * + * The literal form matters because it is the only spelling the three consuming + * engines share. `\\u{...}` is JS-only: Python re needs `\\U000E0000`, Go needs + * `\\x{E0000}`, and neither of those is valid JavaScript. Sixteen conditions + * across ten rules used it, which made those rules uncompilable from the Python + * and Go channels; they are literals now, and this is what keeps them working + * here. + */ +export function needsUnicodeFlag(pattern: string): boolean { + if (pattern.includes('\\u{') || pattern.includes('\\p{')) return true; + for (const ch of pattern) { + const cp = ch.codePointAt(0); + if (cp !== undefined && cp > 0xffff) return true; + } + return false; +} + function safeCompile(source: string, flags: string, ruleId?: string): RegExp | null { if (!isReDoSSafe(source)) { console.warn( diff --git a/tests/validate-rules.ts b/tests/validate-rules.ts index 700b7166b..c1eb577cc 100644 --- a/tests/validate-rules.ts +++ b/tests/validate-rules.ts @@ -275,8 +275,15 @@ function validateRule(filePath: string): ValidationResult { // Strip leading inline flags (JS uses RegExp flags instead) pattern = pattern.replace(/^\(\?[imsx]+\)/, ''); try { - // Use 'u' flag when pattern contains \u{XXXXX} or \p{} β€” matches ATR engine behaviour - const needsUnicode = /\\u\{|\\p\{/.test(pattern); + // Use 'u' when the pattern needs it. \u{...} and \p{...} need it by + // syntax; so does a literal astral character, because without 'u' a + // class range written with literals is a SyntaxError rather than a + // range. Literals are the spelling ATR rules use, since \u{...} is + // JS-only and unusable from the Python and Go channels β€” keep this + // in step with needsUnicodeFlag in src/engine.ts. + const needsUnicode = + /\\u\{|\\p\{/.test(pattern) || + [...pattern].some((ch) => (ch.codePointAt(0) ?? 0) > 0xffff); new RegExp(pattern, needsUnicode ? 'u' : ''); } catch (e) { const desc = cond['description'] ?? cond['field'] ?? 'unknown';