Repository navigation
chore(deps): update dependency soupsieve to v2.9 [security] - #1103
Open
renovate-bot wants to merge 1 commit into
Open
renovate-bot wants to merge 1 commit into
renovate-bot wants to merge 1 commit into
Conversation
This branch has not been deployed
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.
This PR contains the following updates:
2.8.4→2.9Soup Sieve: Polynomial-time ReDoS (O(n²)) in the whitespace/comment trimming regex
RE_WS_END(triggers on VALID selectors)CVE-2026-85999 / GHSA-j934-xhv5-fg8f
More information
Details
Summary
Before tokenizing,
selector_itertrims leading/trailing whitespace and comments by running two regexes over the whole raw selector with.search(). The trailing one,RE_WS_END = re.compile(r'{WSC}*$'), is anchored only at the end ($), not the start. Because.search()retries the pattern at every offset, a long run of whitespace or CSS comments that is not sitting exactly at the end of the string makes each retry greedily consume the run and then fail$, producing O(n²) time. This triggers on perfectly valid selectors — e.g. a descendant combinator with a long whitespace gap,a+" "*n+b— so no malformed input is required. A single valid ~20 KB selector stalls the interpreter for ~10 s of CPU.Trust model (Q0)
The selector string is the input, reaching this code via
soupsieve.compile(), thesoupsieve.select/iselect/match/filterhelpers, and BeautifulSoup'ssoup.select(selector)/soup.select_one(selector). Exploitable wherever an application passes a user-controlled CSS selector to BeautifulSoup/soupsieve. Applications using only hard-coded selectors are unaffected.Root cause (exact anchors) —
src/soupsieve/css_parser.pyWSC = (?:{WS}|{COMMENTS}). ForRE_WS_END = (?:WS|COMMENTS)*$,.search()walks start offsets 0..n. Whenever the offset lands inside a long whitespace/comment run,(?:WS|COMMENTS)*greedily consumes to the run's end, then$fails (a non-whitespace char follows), the engine backtracks the whole run, the offset advances by one, and the work repeats — O(n) offsets × O(n) per attempt = O(n²).RE_WS_BEGINavoids this because^pins it to a single start offset.The intent (trim trailing whitespace/comments) can be met with an anchored/loopless approach; the current unanchored
.search()of a*$pattern is the defect.Reproduction environment (discipline #12 — published artifact)
751c57b(2.9,PYTHONPATH=src):cd src && python3 ../poc/poc_redos_ws_trim.py.soupsieve 2.8.4(freshuv pip install soupsieve beautifulsoup4):cd poc && ../.venv-published/bin/python poc_redos_ws_trim.py→ same O(n²) (evidence:poc/evidence_redos_ws_trim_PUBLISHED_2.8.4.log).PoC (
poc/poc_redos_ws_trim.py)Isolated confirmation that the cost is in
RE_WS_END.searchspecifically (poc/isolate_ws_trim.py):RE_WS_ENDon"div"+" "*n+">"is O(n²) (2000→100 ms, 4000→448 ms, 8000→1622 ms, 16000→6719 ms), while the start-anchoredRE_WS_BEGINon" "*n+"x"stays linear (32000→1.5 ms). Profilingcompileshows the entire wall time in 2re.Pattern.searchcalls, not.match.Evidence — HEAD 2.9 (verbatim
poc/evidence_redos_ws_trim.log)Evidence — published 2.8.4 (verbatim
poc/evidence_redos_ws_trim_PUBLISHED_2.8.4.log)Impact — calibrated
compile()/select()call on an attacker-controlled selector, triggered by a long internal whitespace or CSS-comment run. ~8 KB → ~1.6 s; ~20 KB → ~10 s; scaling ~×4 per input doubling. Notably fires on WELL-FORMED selectors, so it does not depend on a parser error path.Distinction from the IDENTIFIER/VALUE ReDoS
This is a separate root cause and a separate fix: the cost here is entirely in the
RE_WS_END = {WSC}*$trim step run with.search()before tokenizing (measured inre.Pattern.search), whereas the IDENTIFIER/VALUE issue is adjacent-quantifier backtracking during token.match(). They can be fixed independently.Remediation
.search()of{WSC}*$, scan trailing whitespace/comments from the end directly (e.g. reverse scan, orre.compile(r'^{WSC}*').matchon a reversed-equivalent), so no per-offset retry occurs.*$search.Severity
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:LReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
Soup Sieve: Polynomial-time ReDoS (O(n²)) in the
IDENTIFIER/VALUEselector sub-patternsCVE-2026-86000 / GHSA-gjv8-xp57-g29c
More information
Details
Summary
soupsieve compiles CSS selector strings with a set of hand-written regular expressions. The shared
IDENTIFIERsub-pattern (also embedded inVALUE, and therefore in attribute selectors) places two adjacent quantified groups over overlapping character classes:(?:[classA]|ESC)+(?:[classB]|ESC)*, where both classes match ordinary identifier characters such asa. When a selector contains a long identifier/value run that must ultimately fail to match (e.g. an attribute value with no closing], or an identifier followed by an invalid character), the regex engine backtracks across all O(n) ways to split the run between the+group and the*group, giving O(n²) parse time. A single attacker-controlled selector of a few kilobytes stalls the interpreter for many seconds of CPU; tens of kilobytes reach minutes.Trust model (Q0)
The selector string is the input. It reaches this code via
soupsieve.compile(),soupsieve.select/iselect/match/filter, and — most commonly — BeautifulSoup'ssoup.select(selector)/soup.select_one(selector), which delegate to soupsieve. This is exploitable in any application that passes a user-controlled CSS selector to BeautifulSoup/soupsieve (scrapers that accept selectors, no-code extraction tools, admin/query UIs). Applications that only use hard-coded selectors are not affected.Root cause (exact anchors) —
src/soupsieve/css_parser.py[^\x00-\x2f\x30-\x40\x5B-\x5E\x60\x7B-\x9f]excludes digits (0x30-0x39); classB[^\x00-\x2c\x2e\x2f\x3A-\x40\x5B-\x5E\x60\x7B-\x9f]allows digits. The intent is "first char not a digit, remaining chars may be digits."a= 0x61). The construct is therefore effectively(?:C)+(?:C)*over an overlapping class C — the canonical adjacent-quantifier shape that backtracks quadratically on a failing match.The quadratic only manifests when the overall match must fail.
IDENTIFIERmatched greedily on"a"*nsucceeds in linear time (~1 ms at n=32000). Anchoring it so a following element is mandatory and fails (IDENTIFIER + "$"against"a"*n + "!") reproduces the O(n²) directly: n=2000 → 44 ms, 4000 → 257 ms, 8000 → 743 ms, 16000 → 2944 ms (~×4 per ×2). Profilingcompile("[a=" + "a"*4000)shows only 12re.matchcalls consuming 2.685 s — i.e. the cost is inside a single regex match, confirming regex backtracking (not loop overhead).Reproduction environment (discipline #12 — published artifact)
751c57b(2.9,PYTHONPATH=src):cd src && python3 ../poc/poc_redos_compile.py.soupsieve 2.8.4(freshuv pip install soupsieve beautifulsoup4):cd poc && ../.venv-published/bin/python poc_redos_compile.py→ same O(n²) (evidence:poc/evidence_redos_compile_PUBLISHED_2.8.4.log).PoC (
poc/poc_redos_compile.py)End-to-end note:
bs4.BeautifulSoup(html).select(payload)reaches the samecompile()path, so the stall is triggerable directly through BeautifulSoup with a user-supplied selector. Verified on bs4 4.15.0 + soupsieve 2.8.4:soup.select("[a=" + "a"*6000)took ~5.0 s for one call (evidence:poc/evidence_bs4_select_PUBLISHED_2.8.4.log).Evidence — HEAD 2.9 (verbatim
poc/evidence_redos_compile.log)Evidence — published 2.8.4 (verbatim
poc/evidence_redos_compile_PUBLISHED_2.8.4.log)Impact — calibrated
compile()/select()call on an attacker-controlled selector. ~8 KB → ~8 s; ~12 KB → ~17 s; scaling ~×4 per input doubling. A handful of such requests exhausts a worker/thread and degrades or stalls the service (single-threaded regex holds the GIL).Remediation
IDENTIFIER: match a single leading non-digit character then the remaining class once, e.g.(?:-?(?:[classA]|ESC)(?:[classB]|ESC)*|--(?:[classB]|ESC)*), so no+/*pair spans the same characters.(?>...),*+) to forbid backtracking into the identifier run.Severity
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:LReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
Release Notes
facelessuser/soupsieve (soupsieve)
v2.9Compare Source
2.9
:nth-child/:nth-of-type(and-last-variants) forAn+Bvalues whose sequence steps ontoindex 0 or onto the last child (e.g.
:nth-child(2n-2),:nth-child(n-1),:nth-child(n+5)), which previouslymatched the wrong elements or nothing at all (@gaoflow).
Configuration
📅 Schedule: (in timezone UTC)
🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.
♻ Rebasing: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.
🔕 Ignore: Close this PR and you won't be reminded about this update again.
This PR was generated by Mend Renovate. View the repository job log.