Fix regex injection in StringDetector word-match (re.escape)#1882
Closed
AUTHENSOR wants to merge 1 commit into
Closed
Fix regex injection in StringDetector word-match (re.escape)#1882AUTHENSOR wants to merge 1 commit into
AUTHENSOR wants to merge 1 commit into
Conversation
47f5370 to
835e078
Compare
StringDetector's matchtype='word' path built a regex via raw interpolation:
re.search(r'\b' + s + r'\b', text). Substrings containing regex metacharacters
were treated as regex, not literals. This affects the 18 Surge/Ofcom safety
detectors (unsafe_content.py) that use matchtype='word' with externally-
sourced profanity/hate-speech word lists.
Two failure modes, both confirmed against the bundled Surge list:
- CRASH: triggers like '(' or '[' (unbalanced) raise re.error, aborting the
detector.
- MIS-MATCH: 25 Surge-list terms contain metachars. e.g. 'c*nt' compiles as
'zero-or-more c, then nt' and matches the benign word 'nt' (false
positive); 'bi+ch' matches 'bich'; 's.o.b.' matches 'sxobx'.
Fix: re.escape(s) before interpolation, so triggers are matched literally.
Also: normalize case once per output (lower() outside the substring loop) —
the previous in-loop lowering was a latent bug masked by lower()'s
idempotency. 11 tests added (regression + fix + backward-compat).
Signed-off-by: John Kearney <johndanielkearney@gmail.com>
835e078 to
91ab870
Compare
Collaborator
|
This duplicates #1880 why would this version be better? |
Collaborator
|
PR seems a little overwrought; Prefer the fix in #1880. Closing. |
Author
|
Fair point, the core fix (re.escape(s) in the word-match path) is the same as #1880, and I missed that an open PR already addressed it. Apologies for the noise. The one additional thing I'd note (happy to raise it on #1880 rather than here): the surrounding case-normalization also has a latent issue, output_text is re-lowered inside the substring loop on every iteration (works only because .lower() is idempotent). The fix in #1880 doesn't touch that, so it's worth a follow-up. The extra test coverage and the confusables table from this PR are also available if useful, but no objection to the minimal fix landing first. |
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.
Fix regex injection in
StringDetectorword-match (re.escape)Summary
StringDetector'smatchtype="word"path builds its match regex by rawinterpolation:
re.search(r"\b" + s + r"\b", text). Substrings containingregex metacharacters are treated as regex, not literals. This affects the 18
Surge/Ofcom safety detectors in
unsafe_content.pythat usematchtype="word"with externally-sourced profanity/hate-speech word lists —two of those lists contain metacharacters, causing crashes and silent
mis-matches in a production safety scanner.
The fix is
re.escape(s)so triggers are matched literally.The bug
re.escapeis never applied, so the trigger substringsis compiled as aregex. Two failure modes, both confirmed against the bundled Surge profanity
list (2,105 terms):
1. Crash (
re.error)Any trigger that is an invalid regex aborts
detect()for the whole attempt:2. Silent mis-match (false positives / negatives)
The Surge list contains 25 terms (19 unique) with regex metacharacters.
Each compiles to a regex that matches the wrong text:
c*ntc* nt(0+ c's)"nt"→ flagged profanebi+chbi+ ch(1+ i's)s.o.b.s.o.b.(.= any char)pu$sypu$ sy(end-anchor)twa+twa+(1+ a's)The worst case:
c*ntmatches the benign English word "nt", so any modeloutput containing the standalone word "nt" trips the
SurgeProfanitySexualdetector. The safety scan produces wrong verdicts.
Reproduction (verified against the real detector)
The fix
Also refactored case normalization: previously
output_textwas loweredinside the substring loop (re-lowered N times).
str.loweris idempotent soit happened to be correct, but it was a latent bug. Now lowered once per output
into a
search_textvariable.Tests
Appended to
tests/detectors/test_detectors_base.py(11 new tests; the 10existing tests in that file pass unchanged — confirming no behavior regression):
@pytest.mark.parametrize, 5 cases):(result,(,[,a{2,1},(*— each used to raisere.error; now treated as literals.c*ntno longer matches "nt";bi+chno longermatches "bich";
s.o.b.no longer matches "sxobx".c*ntmatches the literal "c*nt" after escape.matchtype="str"unaffected (no regex path).
Impact
unsafe_content.pyclasses that setmatchtype="word"(Surge and Ofcom profanity categories, LDNOOBW,SlursReclaimedSlurs), plus any third-party detector subclassing
StringDetectorwith the word matchtype.metacharacters. The OFCOM list has 0.
benign text ("nt") or miss real terms (
pu$sy) is unreliable. No securityimpact beyond the scan itself.
Scope
Single-purpose: one fix in
StringDetector.detect+ tests appended to theexisting
test_detectors_base.py. Diff: +112/−4 across 2 files. No changes toany detector class, probe, or evaluator.
Notes
matchtype="str"andstartswithpaths are unaffected (they usein/.startswith, no regex).TriggerListDetectoris unaffected (also usesin, no regex).blackformatting passes; thematchtype="word"word-boundary semanticsmean terms starting/ending in non-word chars (like
(result)ors.o.b.)don't match their own literal under
\b— this is a pre-existing propertyof the word matchtype, not changed by this fix (documented in the test
comments).
test_detectors_base.pypass identically before/after, and the broaderdetector suite (440 tests) shows no regression.