high · 7.5 Patched

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 · published 20 Oct 2026

How serious is it

  • Severity: high, CVSS score 7.5 out of 10.
  • No exploit prediction (EPSS) score yet.
  • Not on the CISA list of exploited vulnerabilities.
  • No public exploit in Exploit-DB.

Is your app affected by CVE-2026-111758?

Paste this prompt into Claude Code, Cursor or any AI coding tool: it runs a free Shippable scan of your project and fixes what it finds. Built with Lovable, Bolt or v0? Scan your live app. Or upload your package file.

Who is affected

  • markdown-it (npm) >= 14.1.0, < 14.3.3
  • markdown-it (npm) >= 15.0.0, < 15.0.3

How to check

  1. Run npm ls markdown-it in your project to see the installed version, including copies pulled in by other packages.
  2. Or paste your lockfile into the checker: it lists every package with a known vulnerability, this one included.
  3. Or run npx shippable-dev scan localhost:3000 in your project folder. It reads your lockfile and flags this vulnerability along with every other known one.

How to fix

Copies a prompt for Claude Code, Cursor or Codex: it checks your lockfiles for this and upgrades it.
  1. On >= 14.1.0, < 14.3.3: Upgrade markdown-it to 14.3.3 or later, commit the lockfile and redeploy.
  2. On >= 15.0.0, < 15.0.3: Upgrade markdown-it to 15.0.3 or later, commit the lockfile and redeploy.
  3. If a vulnerable version was live, check your logs and rotate the secrets that server could read.

The advisory

### 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: ```javascript // 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.

Check your whole app

Dependencies are only half of it. Free and read-only: before you push, run npx shippable-dev scan localhost:3000 in your project folder, no account needed. For your live app, you prove it's yours with a one-line tag first.

References

Merged from GitHub Advisory Database. Last updated 23 Oct 2026. All vulnerabilities · RSS · JSON