Repository navigation
security: cap :not()/:has()/:host() recursion depth and :is() output count - #274
Merged
Merged
Conversation
…count Continuing the same audit as the calc() fix in this branch: two more text-level CSS parsers recurse on attacker-controlled nesting depth with no guard. parseSimple/parseSimpleSelectorList/parseHasSelectorList (the :not()/:has()/:host() argument parsers) form one mutually recursive cycle with no depth check -- the same shape as calc()'s, but each level's own string-scanning cost compounds, making it closer to quadratic than linear: a 500,000-deep :not(:not(...)) chain used to time out past 60s and now completes in ~2.5s. Fixed with a depth parameter threaded through all three, capped at maxSelectorNestingDepth (256, matching calc()'s own cap). expandFunctionalPseudos (:is()/:where()/:matches()) is a different, more severe failure mode: it doubles its OUTPUT COUNT at every level a comma-containing wrapper nests inside another one of itself -- n levels of self-nesting give 2^n alternatives from a selector whose own TEXT depth is just n. 20 such levels (elaborate-looking, not obviously malicious) already produces 1,048,576 alternatives and takes several seconds. A depth cap alone would not have been enough, since the depth itself stays small while the output explodes -- fixed instead with a shared output budget (expandFunctionalPseudosBudget, maxSelectorExpansions = 4096) threaded through the whole expansion, truncating the result once the TOTAL alternatives produced hits the cap. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
4 tasks done
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.
Summary
Continuing the same audit as the just-merged calc() fix (#273): two more text-level CSS parsers recurse (or expand) on attacker-controlled nesting depth with no guard.
:not()/:has()/:host():parseSimple/parseSimpleSelectorList/parseHasSelectorListform one mutually recursive cycle with no depth check — the same shape ascalc()'s, but each level's own string-scanning cost compounds (closer to quadratic than linear): a 500,000-deep:not(:not(...))chain used to time out past 60s, now completes in ~2.5s. Fixed with a depth parameter threaded through all three, capped atmaxSelectorNestingDepth = 256(matchingcalc()'s own cap).:is()/:where()/:matches()— a different, more severe failure mode:expandFunctionalPseudosdoubles its OUTPUT COUNT at every level a comma-containing wrapper nests inside another one of itself (2^nalternatives fromnlevels of text nesting). 20 such levels — elaborate-looking, not obviously malicious — already produces 1,048,576 alternatives and takes several seconds. A depth cap alone wouldn't help since the depth stays small while the output explodes; fixed with a shared output budget (maxSelectorExpansions = 4096) threaded through the whole expansion, truncating once the TOTAL alternatives produced hits the cap.Both fail the same way their parser already fails on any other malformed/too-much-to-handle input (truncate/drop, never crash), matching this codebase's existing "reduce, don't drop" convention.
Test plan
go build ./...go test ./...(all packages pass)TestParseSimpleDoesNotCrashOnPathologicallyDeepNotNesting,TestExpandFunctionalPseudosCapsCombinatorialBlowup(+ a direct white-box test for its budget-exhausted entry guard)csspackage coverage unaffected (99.5% baseline); every new branch at 100%🤖 Generated with Claude Code