GitHub Advisoryadvisory · PyPI
GHSA-v7cf-c9rm-wm3j Uncontrolled recursion DoS in JustHTML() via deeply nested HTML
HIGH
Official page ↗
- Aliases
- CVE-2026-9769
- Ecosystem
- PyPI
- Packages
- justhtml
- Severity
- High
- CVSS vector
- CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:P/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N
- Weaknesses
- CWE-674
- Reviewed
- 2026-03-17
Affected packages
• PyPI justhtml: < 1.10.0 → fixed in 1.10.0
Description
Summary
justhtml through 1.9.1 allows denial of service via deeply nested HTML. During parsing, `JustHTML.__init__()` always reaches `TreeBuilder.finish()`, which unconditionally calls `_populate_selectedcontent()`. That function recursively traverses the DOM via `_find_elements()` / `_find_element()` without a depth bound, allowing attacker-controlled deeply nested input to trigger an unhandled `RecursionError` on CPython. Depending on the host application's exception handling, this can abort parsing, fail requests, or terminate a worker/process.
Details
`TreeBuilder.finish()` (`treebuilder.py#L476`) unconditionally calls `_populate_selectedcontent(self.document)` at line 494. `_populate_selectedcontent()` (`treebuilder.py#L1243`) calls `_find_elements()` (`treebuilder.py#L1280`) to recursively search the DOM tree for `` elements:
```python
def _find_elements(self, node: Any, name: str, result: list[Any]) -> None:
"""Recursively find all elements with given name."""
if node.name == name:
result.append(node)
if node.has_child_nodes():
for child in node.children:
self._find_elements(child, name, result) # recursive call
```
When the DOM tree depth exceeds CPython's default recursion limit (1000), this raises an unhandled `RecursionError`. The full call path is:
`JustHTML(html)` → `tokenizer.run()` → `tree_builder.finish()` → `_populate_selectedcontent(document)` → `_find_elements(root, "select", selects)` (recursive)
Deeply nested DOM trees can be produced by nesting `` tags ~1000 levels deep. On CPython with the default recursion limit, approximately 11 KB of `` nesting is sufficient to trigger the error. The exact depth threshold is environment-dependent (CPython version, recursion limit setting, call stack depth at invocation).
Additional recursive functions are affected on already-parsed deep trees:
• `Node.clone_node(deep=True)` (`node.py#L523`) — called during sanitization
• `_node_to_html()` (`serialize.py#L580`) — used by `to_html(pretty=True)`
• `_to_markdown_walk()` (`node.py#L817`) — used by `to_markdown()`
Note: the library already uses iterative traversal in several comparable functions (e.g., `_node_to_html_compact` at `serialize.py#L197`, `_to_text_collect` at `node.py#L161`, `_is_blocky_element` at `serialize.py#L405`, `apply_to_children` at `transforms.py#L1642`), demonstrating the correct pattern.
PoC
```python
from justhtml import JustHTML
html = "" * 1000 + "x" + "" * 1000
doc = JustHTML(html) # raises RecursionError
```
Test environment: CPython 3.14.3, macOS ARM64 (Apple Silicon), justhtml 1.9.1, default recursion limit (1000)
| Input | Size | Result |
|-------|------|--------|
| `` × 500 | 5,501 bytes | OK |
| `` × 800 | 8,801 bytes | OK |
| `` × 1000 | 11,001 bytes | RecursionError |
The error occurs with both `sanitize=True` (default) and `sanitize=False`.
Impact
An attacker who can supply HTML for parsing can trigger an unhandled `RecursionError` during `JustHTML()` construction. The error is triggered during construction and is not avoided by `justhtml` configuration alone; mitigating it requires host-application exception handling or input constraints. Depending on the host application's exception handling, this can abort parsing, fail requests, or terminate a worker/process.
Suggested Fix
Convert the recursive tree traversal functions to iterative implementations using an explicit stack. Example for `_find_elements`:
```python
def _find_elements(self, node: Any, name: str, result: list[Any]) -> None:
stack = [node]
while stack:
current = stack.pop()
if current.name == name:
result.append(current)
if current.has_child_nodes():
stack.extend(reversed(current.children))
```
The same conversion should be applied to `_find_element`, `clone_node(deep=True)`, `_node_to_html()`, and `_to_markdown_walk()`.
Related
Alias · CVE (1)
- CVE-2026-9769 justhtml through 1.9.1 (fixed in 1.10.0) is vulnerable to uncontrolled recursion leading to denial of service. vulnerability
Weakness · CWE (1)
- CWE-674 Uncontrolled Recursion weakness
References
- https://github.com/EmilStenstrom/justhtml/security/advisories/GHSA-v7cf-c9rm-wm3j web
- https://nvd.nist.gov/vuln/detail/CVE-2026-9769 advisory
- https://github.com/EmilStenstrom/justhtml package
- https://github.com/EmilStenstrom/justhtml/releases/tag/v1.10.0 web
- https://www.vulncheck.com/advisories/justhtml-before-denial-of-service-via-deeply-nested-html web