fix(compiler): prevent ReDoS in ShadowCSS _ruleRe regexp#69827
Open
CYANO-01 wants to merge 1 commit into
Open
Conversation
The `_ruleRe` regular expression used by `processRules` in the ShadowCSS
emulation pipeline contained two backtracking traps that combine to produce
O(k²) behaviour when a CSS string contains `k` comment-placeholder tokens
followed by content without a terminating `;` or `{`.
Root causes
-----------
1. Nested quantifier in group 1:
`(\s*(?:%COMMENT%\s*)*)`
The outer `*` and the two inner `\s*` terms can all consume the same
whitespace characters. When the overall match fails, the engine
redistributes those spaces across the three sub-patterns, producing O(k²)
backtracks for a string of k comment tokens.
2. Lazy group 2 + unconstrained boundary:
`([^;\{\}]+?)` can start with whitespace, so the engine can also
redistribute leading spaces between group 1 and group 2. Combined with
the trailing `(\s*)` of group 3, both boundaries are ambiguous under
backtracking.
Benchmark (Node.js 24 / V8):
k (comment tokens) input length before after
------------------ ------------ --------- ------
25 335 chars 5.8 ms 0.1 ms
50 610 chars hangs* 0.3 ms
100 1 160 chars hangs* 1.2 ms
200 2 260 chars hangs* 4.6 ms
* timed out in benchmark loop
Fix
---
Replace the vulnerable pattern with three coordinated changes:
Before: `(\s*(?:%COMMENT%\s*)*)([^;\{\}]+?)(\s*)(...)`
After: `(?=[^;\{\}]*[;\{])((?:%COMMENT%|\s)*)([^;\{\}\s][^;\{\}]*?)(\s*)(...)`
1. Lookahead `(?=[^;\{\}]*[;\{])` — fail immediately when no terminator
exists anywhere ahead. `processRules` only ever needs to match complete
CSS rules (which always end with `;` or `{`), so this lookahead introduces
no false negatives while cutting out all the backtracking work for
malformed or partially-escaped input.
2. Flat group 1 `((?:%COMMENT%|\s)*)` — replaces the nested-quantifier form
with a simple alternation; each position is either a whitespace character
or the start of the literal placeholder, with no overlap.
3. Non-whitespace-anchored group 2 `([^;\{\}\s][^;\{\}]*?)` — requires the
selector/declaration to begin with a non-whitespace character, making the
boundary between group 1 and group 2 unambiguous so the engine cannot
redistribute leading spaces between them.
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.
Problem
The
_ruleReregular expression used byprocessRulesin the ShadowCSS emulation pipeline (packages/compiler/src/shadow_css.ts) contains two backtracking traps that combine to produce O(k²) behaviour when a CSS string haskcomment-placeholder tokens followed by content without a terminating;or{.Root causes
1. Nested quantifier in group 1
The outer
*and the two inner\s*terms can all consume the same whitespace characters. When the overall match fails the engine redistributes those spaces across the three sub-patterns.2. Lazy group 2 with unconstrained boundary
Combined with the trailing
(\s*)group, the engine can also redistribute leading whitespace between group 1 and group 2, doubling the search space.Benchmark (Node.js 24 / V8)
† timed out
10× input → ~54× time → O(k²) confirmed before the fix; O(k) after.
Fix
Three coordinated changes to
_ruleRe:Lookahead
(?=[^;\{\}]*[;\{])— fails immediately when no terminator exists anywhere ahead.processRulesonly ever matches complete CSS rules (which always end with;or{), so this lookahead introduces no false negatives while cutting all backtracking work on malformed/partial input.Flat group 1
((?:%COMMENT%|\s)*)— replaces the nested-quantifier form. Each position is either whitespace or the start of the literal placeholder; no overlap, no ambiguity.Non-whitespace-anchored group 2
([^;\{\}\s][^;\{\}]*?)— requires the selector/declaration to begin with a non-whitespace character, making the boundary between group 1 and group 2 unambiguous.Related
Similar to #69763 (
SOURCEMAP_URL_REGEXPReDoS inplatform-browser).A regression test is included in
process_rules_spec.ts.