Summary
Every < in a document makes the inline parser look for the end of the construct it might start, and the scan is thrown away when that end is not there. On input made of < whose construct never ends, each one walks to the end of the input, so parsing is quadratic in the size of the input. Roughly 800 KB of x <!-- > keeps a core busy for over a minute, so a single request can stall a worker in any application that renders untrusted Markdown (comments, chat, wikis).
Details
HtmlInlineParser is triggered by < and has four scans of this shape:
| Construct |
Looks for |
Code |
| processing instruction |
?> |
L153 |
| comment |
--> |
L177 |
| CDATA section |
]]> |
L196 |
| declaration |
> |
L215 |
Each of them ends up in Scanner.find, which advances one character at a time and only gives up at the end of the input:
while (scanner.find('-') >= 0) { // no `-->` ahead, so this walks to the end of the input
if (scanner.next("-->")) {
return true;
} else {
scanner.next();
}
}
The candidate is then rejected, the position is reset, and the next < starts the same scan over again. With n openers that is n scans of average length n/2. Plain paragraph text is enough to reach it:
| Input, repeated |
Size at 100,000 |
25,000 |
50,000 |
100,000 |
100,000 with the patch |
x <? > |
586 KB |
1.7 s |
6.6 s |
26.7 s |
0.08 s |
x <!-- > |
781 KB |
4.6 s |
19.2 s |
76.7 s |
0.07 s |
x <![CDATA[ > |
1270 KB |
3.5 s |
15.3 s |
63.0 s |
0.13 s |
Measured with Parser.builder().build() and HtmlRenderer.builder().build() on JDK 25, current main (7a68e920). Times roughly quadruple when the input doubles.
Each payload deliberately contains a > close to every <, so that the separately reported autolink scan for > stays cheap and only the inline HTML scanning is measured. It also starts with text so the line doesn't begin with <, which would parse as an HTML block instead.
A PR will shortly follow.
Credits
Found by Team Atlanta.
Collected and verified by OSTIF using AI with their own harness.
Manually verified, fixed, and reported by Shielder.
Summary
Every
<in a document makes the inline parser look for the end of the construct it might start, and the scan is thrown away when that end is not there. On input made of<whose construct never ends, each one walks to the end of the input, so parsing is quadratic in the size of the input. Roughly 800 KB ofx <!-- >keeps a core busy for over a minute, so a single request can stall a worker in any application that renders untrusted Markdown (comments, chat, wikis).Details
HtmlInlineParseris triggered by<and has four scans of this shape:?>-->]]>>Each of them ends up in
Scanner.find, which advances one character at a time and only gives up at the end of the input:The candidate is then rejected, the position is reset, and the next
<starts the same scan over again. With n openers that is n scans of average length n/2. Plain paragraph text is enough to reach it:x <? >x <!-- >x <![CDATA[ >Measured with
Parser.builder().build()andHtmlRenderer.builder().build()on JDK 25, currentmain(7a68e920). Times roughly quadruple when the input doubles.Each payload deliberately contains a
>close to every<, so that the separately reported autolink scan for>stays cheap and only the inline HTML scanning is measured. It also starts with text so the line doesn't begin with<, which would parse as an HTML block instead.A PR will shortly follow.
Credits
Found by Team Atlanta.
Collected and verified by OSTIF using AI with their own harness.
Manually verified, fixed, and reported by Shielder.