Honor unless: exclusions in search rules - #77
Open
juangaitanv wants to merge 1 commit into
Open
juangaitanv wants to merge 1 commit into
juangaitanv wants to merge 1 commit into
Conversation
`rules/javascript/frontend_security.ron` ships three `unless:` blocks (lines 20, 64, 98), but `UnifiedRule` had no `unless` field and serde does not set `deny_unknown_fields` — RON parsed them into nothing, so every exclusion was inert and the false positives they name still fired. Add the field and evaluate it in `rule_matches_pattern_unified`, scoped to the matched byte span rather than the whole text: an `unless` string belonging to an unrelated statement on the same line no longer swallows a real finding. Spans are anchored right-to-left so a safe earlier call cannot stretch the scope over a later vulnerable one. Claude-Session: https://claude.ai/code/session_01SEPg7aLSDDusz5xJ22rQJS
leenk7991
approved these changes
Sep 1, 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.
The bug
rules/javascript/frontend_security.ronships threeunless:exclusion lists — lines 20 (js-dom-xss-001), 64 (js-react-dangerously-set-inner-html-001) and 98 (js-dom-xss-003) — butUnifiedRule(src/models.rs) had nounlessfield, and the struct does not setserde(deny_unknown_fields). RON parsed the key into nothing and no error surfaced. Every one of those 26 exclusions has been inert in every scan since it was written: sanitizedinnerHTML,DOMPurify-wrappeddangerouslySetInnerHTML, and static-stringdocument.writeall still reported as DOM XSS.The fix
src/models.rs:190—pub unless: Option<Vec<String>>, with the sameskip_serializing_if/defaultattributes as its neighbors. Search-mode only; taint rules already express this withsanitizers.src/rules.rs:403—rule_matches_pattern_unifiednow gates a positive match onunless. Every finding path inherits it from this one place:check_rule_against_node(src/scanner/scanning_logic.rs:24) and the per-node rule filter (:184). The remaining callers do not produce findings —rule_might_match_function(:573) is a prefilter that runs before the gate, andhas_matching_rules(:612) passes a bare function name, which these rules' positive patterns cannot match.src/rules.rs:422/441/465—matching_patterns,unless_scope_range,tightest_segment_span.Chosen semantics
An exclusion vetoes a finding only when it matches inside the byte span the rule's pattern actually covered — never the whole line.
regex:and escaped-dot forms use the regex match range. Forms that resolve no span (escaped taint patterns) fall back to the whole text, i.e. today's behavior.dangerouslySetInnerHTML*__html*matches a narrower span than*dangerouslySetInnerHTML*user*— and a sanitizer sitting outside the narrowest match still belongs to the same expression.Why not whole-line scoping:
getElementByIdandquerySelectorare themselvesunlessentries ofjs-dom-xss-001. Under line scoping, almost every genuine DOM-XSS finding would be suppressed, because that is how you get the element in the first place.The line-scoping bug this avoids
The reference implementation on the abandoned
html_supportbranch was line-scoped, and review of the old PR #9 flagged it: anyunlessvalue anywhere on the line suppressed unrelated matches on that line. Two concrete regressions, both covered by tests:Both still report. Right-to-left anchoring is what makes the second one work — a leftmost-first span would stretch from the quoted first call to
userin the second and hand the exclusion a quote to veto on.Verification
cargo build --release→ exit 0.make check(clippy fix, format, tests) → exit 0, 278 passed. Fullcargo test→ exit 0: unit 108 → 120, lib 64, harness 28, acceptance 9 scenarios / 34 steps, e2e 5, integration 9, strictness 51, doctest 1 — all unchanged from the pre-edit baseline on this branch's base.End-to-end against the release binary on a JS fixture: sanitized
innerHTMLand staticdocument.writeno longer report; the raw assignment, the rawdocument.write, and the assignment sharing a line withaddEventListenerall still do.New tests in
tests/unit/unless_exclusion_tests.rs:frontend_rules_parse_their_unless_listsinnerhtml_rule_excludes_sanitized_assignmentDOMPurify.sanitize)dangerously_set_inner_html_rule_excludes_sanitized_htmldocument_write_rule_excludes_static_stringdocument\.write\(['"])*_reports_*(3 tests)unrelated_unless_text_on_the_same_line_does_not_suppressaddEventListener)safe_document_write_earlier_on_the_line_does_not_suppresssanitized_sibling_expression_does_not_suppress_a_raw_onerule_without_unless_is_unaffected,empty_unless_list_never_suppressesunlessThe existing
UnifiedRuleliterals in three unit test files gainedunless: None; nothing else changed.Notes
"window\\.location\\.href(?!.*=)"(frontend_security.ron:31) is permanently inert — Rust'sregexcrate has no lookahead,Regex::newreturnsErr, andmatches_regexreturns false. Left alone; not this PR's scope.deny_unknown_fieldswould have caught this class of bug at load time, but it would also hard-fail any rule file carrying a stray key. Worth doing deliberately as a follow-up, not as a side effect here.pattern_match_range/first_positive_match_rangeare deliberately untouched — they feed markup line attribution and must keep mirroringmatches_unified_pattern's "no range for glob" behavior, or markup findings move lines.fusion/vendor/sighthound/is not refreshed here; vendoring follows separately per repo convention (build_all_platforms.sh).https://claude.ai/code/session_01SEPg7aLSDDusz5xJ22rQJS