Skip to content

chore(deps): update dependency markdown-it to v15.0.3 [security] - #481

Open
renovate[bot] wants to merge 1 commit into
mainfrom
renovate/npm-markdown-it-vulnerability
Open

renovate[bot] wants to merge 1 commit into
mainfrom
renovate/npm-markdown-it-vulnerability

Conversation

@renovate

@renovate renovate Bot commented Oct 21, 2026

Copy link
Copy Markdown
Contributor

This PR contains the following updates:

Package Change Age Confidence
markdown-it 15.0.2 → 15.0.3 age confidence

markdown-it: Quadratic-time rendering of a single multi-line link reference definition with default options (regression since 14.0)

CVE-2026-111758 / GHSA-gfr2-rv44-r23c

More information

Details

Summary

A link reference definition whose label or title is never closed is O(n²) in markdown-it 15.0.2 with default options. Continuation lines are pulled lazily with str += lineContent in src/rules_block/reference.ts, and the subsequent charCodeAt and slice calls flatten the V8 cons-string, copying everything collected so far; this runs before the rule decides the definition is invalid. It is a regression introduced by PR #​1004 in 14.0.0: 13.0.2 processed a 313 KB input in 125 ms, while every version since takes about 11 seconds on the same input. At the sizes below, 200 KB takes 3.6 seconds, 400 KB takes 18.1 seconds, and 800 KB takes 74.0 seconds, which is roughly 4–5× per doubling. No options are required to trigger it.

Details

src/rules_block/reference.ts appends continuation lines with str += lineContent at lines 74, 83, 100, 133, and 150, then keeps indexing the accumulated string via str.charCodeAt(pos) in the label and whitespace loops and str.slice(...) in parseLinkTitle. V8 represents += as a cons-string, and the next charCodeAt or slice flattens it, copying everything collected so far, so one definition spanning n lines costs O(n²). Isolation confirms the rule is responsible: block-only parsing with inline disabled is still quadratic, md.disable('reference') makes the same input linear, and the same text without the leading [ is linear. The fix is to track positions in state.src instead of building str, or to collect line slices in an array and parse across them. At minimum, stop scanning the label after 999 characters (CommonMark §6.3) before pulling more lines, and cap title continuation. Add a pathological test with a 100k-line unclosed title and label. Open PR #​1215 edits the same file but its label check runs after the label loop, so it does not bound either variant and the fix can land together with that PR.

The observed output from PoC was:
unclosed title n=100000 200009 B 3585.5 ms
unclosed title n=200000 400009 B 18107.1 ms (5.05x)
unclosed title n=400000 800009 B 73954.4 ms (4.08x)

Regression comparison at 313 KB: 13.0.2 → 125 ms, 14.1.0 → 11.4 s, 15.0.0 → 10.6 s, 15.0.2 → 10.7 s.

Proof of concept

How to execute the PoC:

  1. mkdir -p markdown-it-poc && cd markdown-it-poc
  2. npm init -y
  3. npm i markdown-it@15.0.2
  4. node poc_markdown_it.js

PoC below is:

// poc_markdown_it.js
// Requires: npm install markdown-it@15.0.2
const MarkdownIt = require('markdown-it');
const md = new MarkdownIt();

function bench(label, src) {
  const t0 = process.hrtime.bigint();
  md.render(src);
  const t1 = process.hrtime.bigint();
  console.log(
    `${label.padEnd(30)} ${String(src.length).padStart(9)} B ${(Number(t1 - t0) / 1e6).toFixed(1).padStart(9)} ms`
  );
}

// Unclosed title: one reference definition spread over many lines
for (const n of [100000, 200000, 400000]) {
  bench(`unclosed title n=${n}`, '[a]: /u "' + 'x\n'.repeat(n));
}

// Unclosed label
for (const n of [5000, 10000, 20000]) {
  bench(`unclosed label n=${n}`, '[' + 'a\n'.repeat(n));
}
Impact

This is an algorithmic-complexity denial of service (CWE-407). Any server that renders user Markdown with markdown-it is affected with default options, including comments, issues, chat, wikis, docs previews, and LLM chat UIs. A ~400 KB request body blocks the Node.js event loop for about 18 seconds and an ~800 KB body for about 74 seconds. This is the same class and attacker model as GHSA-253c, but it is reachable without any opt-in option, since the affected path is a core block rule.

Severity

  • CVSS Score: 7.5 / 10 (High)
  • Vector String: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

References

This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).


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

CVE-2026-111766 / GHSA-vfgh-jcmw-g37q

More information

Details

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/rules_inline/text.ts treats < as a terminator, causing the inline parser to attempt the HTML rule at successive HTML construct openers.

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

const match = state.src.slice(pos).match(HTML_TAG_RE)

The comment, processing instruction, declaration, and CDATA alternatives in src/common/html_re.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:

'</' + '<!--'.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:

npm install markdown-it@15.0.2

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

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

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.

Severity

  • CVSS Score: 3.7 / 10 (Low)
  • Vector String: CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:L

References

This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).


Release Notes

markdown-it/markdown-it (markdown-it)

v15.0.3

Compare Source

Changed
  • Updated dependencies.
Fixed
  • Enforce the 999 Unicode code point limit for reference definition labels,
    partially addressing #​1213.
  • Require whitespace before multiline reference titles.
Security
  • Fixed quadratic complexity when parsing multiline reference definitions.
  • Fixed quadratic complexity when parsing unclosed inline HTML comments,
    processing instructions, declarations and CDATA sections.

Configuration

📅 Schedule: (in timezone America/New_York)

  • Branch creation
    • At any time (no schedule defined)
  • Automerge
    • "after 10am and before 4pm every weekday"

🚦 Automerge: Enabled.

♻ Rebasing: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 Ignore: Close this PR and you won't be reminded about this update again.


  • If you want to rebase/retry this PR, check this box

This PR was generated by Mend Renovate. View the repository job log.

@renovate
renovate Bot requested a review from nsheaps as a code owner October 21, 2026 02:58
@renovate
renovate Bot enabled auto-merge (squash) October 21, 2026 02:58
@github-actions

Copy link
Copy Markdown
Contributor

Release Version Check

🟢 PATCH version bump

Version
Current 0.64.13
Next 0.64.14

This version will be released automatically when this PR is merged to main.

Changelog preview

0.64.14 (2026-10-21)

Chores

  • deps: update dependency markdown-it to v15.0.3 [security] (99bdee2)

@github-actions

Copy link
Copy Markdown
Contributor

Preview Deployment

The web app for this PR has been deployed:

Open Preview

Use this to verify the app works correctly, especially for dependency updates.

github-actions Bot added a commit that referenced this pull request Oct 21, 2026

@henry-nsheaps henry-nsheaps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Looks good — safe security patch bump

  • ✅ Patch bump markdown-it 15.0.2 → 15.0.3, addresses GHSA-gfr2-rv44-r23c (CVSS 7.5) and GHSA-vfgh-jcmw-g37q (CVSS 3.7)
  • ✅ Change is confined to the overrides block in package.json and the matching entry in bun.lock
  • ✅ No direct source imports of markdown-it found (project uses remark/mdast per D-49); override still pins transitive consumers
  • ✅ No API-breaking changes between 15.0.2 and 15.0.3; upstream release notes match this PR's description

Click to expand for full details

Scope

Two lines change, both bumping markdown-it from 15.0.2 to 15.0.3:

  • package.json — in the overrides block (so this pins the version for any transitive consumer)
  • bun.lock — matching lockfile entry

Why this is safe

  1. It's a patch release. Upstream CHANGELOG for 15.0.3 only lists security fixes plus a CommonMark §6.3 label-length enforcement; no API/surface changes.
  2. No direct consumers in this repo. A code search across the repo (gh search code --repo nsheaps/cept 'markdown-it') returns only package.json, bun.lock, CHANGELOG.md, and the two spec docs that discuss the pipeline choice. Per D-49 in docs/specs/requirements/README.md, the markdown pipeline is remark/mdast; markdown-it is pulled only transitively, which is exactly what an overrides entry is for.
  3. The CVEs are real and relevant to any transitive user. GHSA-gfr2-rv44-r23c reaches via default options (unclosed reference definitions); GHSA-vfgh-jcmw-g37q only triggers with html: true. Either way, raising the floor via overrides is the right lever.

What I checked

  • PR title follows Conventional Commits (chore(deps): …) — matches the repo's commit-title gate.
  • Both files updated to the same version (no drift between package.json override and lockfile).
  • No prior reviews to reconcile; no reviewer threads to resolve.

Not blocking, just noting

  • The security / Security check is red at the time of review. I did not use CI as the basis for approval — the diff itself is a security-patch bump — but it's worth a quick glance to make sure that check isn't flagging an unrelated regression before merge.

Follow-ups:

  • P0: None
  • P1: None
  • P2: None

Workflow run · GHSA-gfr2-rv44-r23c · GHSA-vfgh-jcmw-g37q · markdown-it 15.0.3 release

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.

0 participants