CVE-2026-9769High▾ TwilightUncontrolled recursion DoS in JustHTML() via deeply nested HTML
▾ Twilight zone — High severity, or a signal on a lesser flaw
impact 41.3 · likelihood 0.1 · exploitation 0
Need a working PoC? Pro members can cast a request and our team develops one — it lands right here.
Exploit-prediction probability, daily snapshots since Aug 24.
Disclosure to exploitation, from the record and what we observed since indexing it.
Disclosed via OSV
0.3%
Last analysed / modified upstream
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.
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 <select> elements:
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 <div> tags ~1000 levels deep. On CPython with the default recursion limit, approximately 11 KB of <div> 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.
from justhtml import JustHTML
html = "<div>" * 1000 + "x" + "</div>" * 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 |
|---|---|---|
<div> × 500 | 5,501 bytes | OK |
<div> × 800 | 8,801 bytes | OK |
<div> × 1000 | 11,001 bytes | RecursionError |
The error occurs with both sanitize=True (default) and sanitize=False.
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.
Convert the recursive tree traversal functions to iterative implementations using an explicit stack. Example for _find_elements:
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().
justhtml < 1.10.0Upgrade to a patched release:
justhtml 1.10.0Connected by shared product, vendor, weakness, or advisory.
CVE-2026-6827Mediumjusthtml has sanitization bypass in custom policies and programmatic DOM
CVE-2026-4671Lowjusthtml introduces denial-of-service hardening
CVE-2026-5751Lowjusthtml: Mutation XSS with custom foreign-namespace sanitization policies
CVE-2026-8630MediumJustHTML Affected by Mutation XSS via Literal Text Serialization in Raw Text Elements (style/script)
CVE-2026-77088Medium· 6.1justhtml: to_markdown() code-span blank-line breakout enables XSS
CVE-2026-5388Mediumjusthtml includes multiple security fixes