Skip to content

markdown: reject pathologically nested markup - #228

Open
cgzones wants to merge 1 commit into
matze:masterfrom
cgzones:markdown-nesting-limit
Open

cgzones wants to merge 1 commit into
matze:masterfrom
cgzones:markdown-nesting-limit

Conversation

@cgzones

@cgzones cgzones commented Oct 22, 2026

Copy link
Copy Markdown
Contributor

Building the DOM is quadratic in nesting depth and raw HTML from a paste reaches it verbatim, so a megabyte of <div> was a quarter million levels deep and cost around 87 seconds of CPU per request — on a blocking thread the request timeout does not stop. Prose stays around a dozen levels, so cap the depth at 256 and answer with a bad request instead.

The depth is measured by running html5ever's own tree builder, set up as ammonia sets it up, over a sink that keeps nothing but where each node hangs. Estimating it from the tags falls short of the parser at every turn: a close tag naming nothing that is open closes nothing, and neither does one in a comment, an attribute value or a <textarea>; <div/> opens a level; a misnested <b> is reopened inside every element that follows it. Each shortfall hands the sanitizer the very tree the limit guards against, and only the parser itself gets all of them right.

The document goes in a kilobyte at a time and the parse is abandoned once it is too deep, so the check cannot be made to build that tree either. On a megabyte of prose it adds about a tenth to the render.

Building the DOM is quadratic in nesting depth and raw HTML from a paste
reaches it verbatim, so a megabyte of `<div>` was a quarter million levels
deep and cost around 87 seconds of CPU per request — on a blocking thread
the request timeout does not stop. Prose stays around a dozen levels, so
cap the depth at 256 and answer with a bad request instead.

The depth is measured by running html5ever's own tree builder, set up as
ammonia sets it up, over a sink that keeps nothing but where each node
hangs. Estimating it from the tags falls short of the parser at every
turn: a close tag naming nothing that is open closes nothing, and neither
does one in a comment, an attribute value or a `<textarea>`; `<div/>`
opens a level; a misnested `<b>` is reopened inside every element that
follows it. Each shortfall hands the sanitizer the very tree the limit
guards against, and only the parser itself gets all of them right.

The document goes in a kilobyte at a time and the parse is abandoned once
it is too deep, so the check cannot be made to build that tree either. On
a megabyte of prose it adds about a tenth to the render.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

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.

1 participant