Repository navigation
fix(operators): keep the @rx prefilter from rejecting what the regex matches - #27
Merged
Merged
Conversation
…matches trieReconstruct prepends the literal before an alternation to the strings that rawExtractSuffixes took from each branch. A branch such as 0*60; or x0*3c; has a star or a shorter literal in front of the literal that was extracted, so "#"+"60;" was required although the two are not adjacent: "&corazawaf#60;" has no "corazawaf#60;". With SecRxPreFilter On the Imunify360 XSS rule (�*60;|�*3c;|<|...) skipped inputs such as "&corazawaf#60;javascript:" and "&#x3c;input'", a missed detection. The same happened for a suffix behind an optional, a wildcard, or a nested group whose literal is not at its start. Replace rawExtractSuffixes with leadingLiterals, which returns only text anchored at the start of the node and stops at a star, an optional, a class or an assertion. The trie words that were correct (select|set|sleep and the like) are unchanged. Add rows to the false-negative table, a unit test for leadingLiterals, and a property test that compares the operator with and without the prefilter on strings generated from the syntax tree of a set of patterns.
3 of 4 tasks
ggrigoras-ops
marked this pull request as ready for review
October 13, 2026 11:47
cl-vkuznetsov
approved these changes
Oct 14, 2026
This was referenced Oct 20, 2026
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.
Fixes
@rxprefilter false negatives in trie reconstruction. Found while checkingSecRxPreFilter Onagainst a production ruleset.What
With
SecRxPreFilter On, the prefilter rejected inputs the regex matches.trieReconstructprepends the literal before an alternation to the stringsrawExtractSuffixestook from each branch, but a branch such as0*60;orx0*3c;has a star or a shorter literal in front of the literal that was extracted."#"+"60;"was required although the two are not adjacent:<has no#60;.rawExtractSuffixesis replaced byleadingLiterals, which returns only text anchored at the start of the node and stops at a star, an optional, a class or an assertion. The trie words that were correct (select|set|sleep, the SQL keyword alternations) come out the same.Impact
An XSS rule in that ruleset starts with
(?:�*60;|�*3c;|<|&#0*60;|&#x0*3c;|&lt;). With the prefilter on it skipped, among others,<javascript:,<onfocus =and&#x3c;input', which the regex matches: a missed detection. The prefilter is off by default, so a default build is not affected; it is a bug for anyone who setsSecRxPreFilter On. The same mistake applies to a suffix behind an optional, a.*, or a nested group whose literal is not at its start (a(?:b?cd|ef)requiredacd, soabcdwas rejected).How it was found: a local differential test that builds every
@rxpattern of that ruleset with and without the prefilter and compares the answers on inputs generated from the pattern's own syntax tree (1,393 patterns, about 1.46 million inputs, 1,295 disagreements before the fix; the 25 that were printed were all one pattern). The test is not part of this PR.Tests
TestPrefilterNeverCausesFalseNegativesfor the entity pattern and for each shape above. They fail onmain.leadingLiterals/anchored_at_start: what is and is not reported, including0*60;,x0*3c;,ab*c,\belect.TestPrefilterAgreesWithRegexOnGeneratedInputs: a property test. For a set of patterns (the entity rules, the trie shapes, SQL keyword alternations) it generates strings from the syntax tree, mostly ones the regex matches, in seven variants (alone, embedded, with newlines, upper and lower case, padded), and requires the operator with the prefilter to answer like the operator without it. It also fails if the generator produces no matching input for a pattern. Fails onmain.Verification
The same differential test on the fixed code: 1,393 distinct
@rxpatterns of the ruleset, 1,363 of them with a prefilter, minimum length or exact match, 300 generated inputs x 7 variants per pattern: 2,925,300 inputs compared, 2,295,485 of them matched by the regex, 0 disagreements.Locally:
gofmt,go build ./...,go vet ./...,go test ./...,go test -tags coraza.rule.rx_prefilter ./internal/operators/,go test -race ./internal/operators/,tinygo test -short -v ./internal/...with and withoutcoraza.no_memoize,go run mage.go lint. No ADR: this is a bug fix (AGENTS.md).Not included
Anything about the CPU cost itself (see the
@pmzero-copy PR, #26), andminLenfor a case-insensitive non-ASCII literal that folds to ASCII ((?i)ſ, the Kelvin sign), a similar class of problem that the ruleset checked here does not exercise.