Get a Demo

Let's Patch It!

Book a short call with one our specialists, we'll walk you through how Endor Patches work, and ask you a few questions about your environment (like your primary programming languages and repository management). We'll also send you an email right after you fill out the form, feel free to reply with any questions you have in advance!

CVE

GHSA-v7cf-c9rm-wm3j

Uncontrolled recursion DoS in JustHTML() via deeply nested HTML
Back to all
CVE

GHSA-v7cf-c9rm-wm3j

Uncontrolled recursion DoS in JustHTML() via deeply nested HTML

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 populateselectedcontent(). That function recursively traverses the DOM via findelements() / findelement() 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 populateselectedcontent(self.document) at line 494populateselectedcontent() (treebuilder.py#L1243) calls findelements() (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() → populateselectedcontent(document) → findelements(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:

Note: the library already uses iterative traversal in several comparable functions (e.g., nodetohtmlcompact at serialize.py#L197totext_collect at node.py#L161isblocky_element at serialize.py#L405applytochildren at transforms.py#L1642), demonstrating the correct pattern.

PoC

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.

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 findelements:

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 findelementclone_node(deep=True)nodeto_html(), and tomarkdown_walk().

Package Versions Affected

Package Version
patch Availability
No items found.

Automatically patch vulnerabilities without upgrading

Fix Without Upgrading
Detect compatible fix
Apply safe remediation
Fix with a single pull request

CVSS Version

Severity
Base Score
CVSS Version
Score Vector
C
H
U
-
C
H
U
0
-
C
H
U
-

Related Resources

No items found.

References

https://github.com/EmilStenstrom/justhtml/security/advisories/GHSA-v7cf-c9rm-wm3j, https://github.com/EmilStenstrom/justhtml, https://github.com/EmilStenstrom/justhtml/releases/tag/v1.10.0

Severity

0

CVSS Score
0
10

Basic Information

Ecosystem
Base CVSS
0
EPSS Probability
0%
EPSS Percentile
0%
Introduced Version
0
Fix Available
1.10.0

Fix Critical Vulnerabilities Instantly

Secure your app without upgrading.
Fix Without Upgrading