Skip to content

deps(prod): update dependency markdown-it to v14 [security] - #442

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 Jun 16, 2026 •

Copy link
Copy Markdown
Contributor

This PR contains the following updates:

Package Change Age Confidence
markdown-it ^12.0.4 → ^14.0.0 age confidence

markdown-it: Quadratic complexity DoS in smartquotes rule via replaceAt string operations

CVE-2026-48988 / GHSA-6v5v-wf23-fmfq

More information

Details

Summary

A quadratic time complexity vulnerability exists in markdown-it's smartquotes rule (enabled via the typographer: true option). An attacker can craft a markdown input consisting of consecutive quotation marks that causes the parser to consume excessive CPU time, leading to denial of service.

Details

The vulnerability is in the replaceAt() helper function used by the smartquotes rule in lib/rules_core/smartquotes.mjs:

function replaceAt (str, index, ch) {
  return str.slice(0, index) + ch + str.slice(index + 1)
}

When markdown-it processes a text token containing many quotation marks (either " or ') with typographer: true, the smartquotes rule iterates through each quote character and calls replaceAt() to substitute it with a typographic (curly) quote. Each call to replaceAt() creates three new string slices and concatenates them, which is an O(n) operation where n is the length of the string.

Since this is called once per quote character in the token, and there are n quote characters, the total time complexity becomes O(n^2).

The root cause is that the smartquotes rule modifies token.content in place using string slicing rather than building the result incrementally. The process_inlines() function (line 14) processes each quote in the text token, and for matching quote pairs, calls replaceAt() on both the opening and closing token's content (lines 151-152). When the entire input is a single text token of quote characters, this results in quadratic behavior.

PoC
const md = require('markdown-it');
const instance = md({ typographer: true });

// 160,000 consecutive double-quote characters
const payload = '"'.repeat(160000);

console.time('render');
instance.render(payload);
console.timeEnd('render');
// Output: render: ~21000ms (21 seconds)

// Compare with typographer disabled:
const safe = md({ typographer: false });
console.time('render-safe');
safe.render(payload);
console.timeEnd('render-safe');
// Output: render-safe: ~8ms

Measured timing on a modern system:

  • 10,000 quotes: ~19ms
  • 20,000 quotes: ~51ms
  • 40,000 quotes: ~212ms
  • 80,000 quotes: ~5,430ms
  • 160,000 quotes: ~21,198ms

The scaling is clearly superlinear (quadratic), with the 80K->160K step showing a ~3.9x increase for a 2x input increase, consistent with O(n^2).

Impact

Applications that render user-supplied markdown with typographer: true are vulnerable to denial of service. An attacker can submit a relatively small payload (160KB of quote characters) that causes the server to spend over 21 seconds processing a single request. Repeated submissions can exhaust server CPU resources and prevent legitimate users from being served.

The impact is mitigated by the fact that the typographer option defaults to false and must be explicitly enabled. However, the typographer feature is commonly enabled in production applications that want smart typography, and the markdown-it documentation prominently suggests enabling it.

A suggested fix would be to replace the replaceAt() approach with an array-based or StringBuilder-style approach that collects all replacements and applies them in a single pass, reducing the time complexity to O(n).

Severity

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

References

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


markdown-it linkify: true has two quadratic paths, so a few hundred KB of markdown blocks the event loop for tens of seconds

GHSA-253c-mchw-3w2r

More information

Details

Summary

Two independent quadratic paths in the linkify: true handling. Both are in markdown-it's own code rather than in linkify-it, which stays linear on both payloads.

src/rules_core/linkify.ts calls arrayReplaceAt once per linkified text token, and that rebuilds the whole children array each time. A paragraph of N soft-broken lines is one inline token with about 2N children, so you get N rebuilds over a 2N array. Schema-less emails are what reach it. A http:// link gets consumed by the inline rule first and never arrives as a text token, so those stay linear.

src/rules_inline/linkify.ts runs state.pending.match(SCHEME_RE) at every :// in the source. state.pending only gets truncated once a link is actually produced, so an unregistered scheme leaves it growing and every :// rescans the lot.

Proof of concept

Clean install of 15.0.0 from npm, new MarkdownIt({ linkify: true }).render(payload), Node 24.

N 5,000 10,000 20,000 40,000
'a@b.co\n'.repeat(N), 34KB to 273KB 1.1s 4.7s 19.6s 89s
'a://'.repeat(N), 20KB to 156KB 0.25s 0.76s 3.1s 15.0s

Doubling the input roughly quadruples the time in both. With linkify: false the same inputs run in 25 to 131ms and stay flat.

Controls for the first one, all at N=20000: putting every email in a single text token ('a@b.co ') takes 0.59s, one email per paragraph ('a@b.co\n\n') takes 0.64s, and soft-broken lines with nothing linkifiable take 42ms. So it needs many children AND many of them linkifying. For the second, replaying just the SCHEME_RE calls against the same growing prefixes with no markdown-it involved accounts for 11.7s of the 15s.

Ordinary prose does it too. 'ping a@b.co ok\n'.repeat(20000) is 293KB and takes 31s.

Caveat

linkify is off by default, so this only reaches apps that turn it on.

Impact

Availability only. A few hundred KB of fairly ordinary markdown pins one core for tens of seconds, and because it's quadratic it degrades quickly with size. Nothing is read, written or executed.

Severity

  • CVSS Score: 6.3 / 10 (Medium)
  • Vector String: CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:N/VA:L/SC:N/SI:N/SA:N

References

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


markdown-it: Quadratic complexity in smartquotes with unmatched closing quotes

CVE-2026-111759 / GHSA-r7fv-28h4-cvq7

More information

Details

Impact

When typographer: true is enabled, specially crafted Markdown can cause quadratic processing time in the smartquotes rule, potentially leading to denial of service in applications processing untrusted input.

Many opening double quotes followed by unmatched closing single quotes cause repeated scans of the entire opener stack. The same issue occurs with the quote types reversed.

The default configuration (typographer: false) is not affected, but this option is enabled with high probability.

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

const md = new MarkdownIt({ typographer: true })

for (const k of [2000, 4000, 8000, 16000, 32000]) {
  const input = '"a '.repeat(k) + "b' ".repeat(k)
  const start = performance.now()
  md.render(input)
  console.log(input.length, (performance.now() - start).toFixed(1), 'ms')
}

Result:

input size time
12000 18ms
24000 47ms
48000 115ms
96000 428ms
192000 1586ms
Patches

Fixed in 15.0.2 and 14.3.2.

Workarounds

Disable typography with typographer: false, or disable only the smartquotes rule:

md.disable('smartquotes')

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 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 the GitHub Advisory Database (CC-BY 4.0).


Release Notes

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

v14.3.3

Compare Source

v14.3.2

Compare Source

v14.3.1

Compare Source

v14.3.0

Compare Source

Changed
  • Reworked build pipeline & tools.
  • Added source maps.
  • Bumped linkify-it to 5.0.2.
Fixed
  • Preserve backslash-space hard line breaks, matching CommonMark 6.7, #​1185.

v14.2.0

Compare Source

Added
  • isPunctCharCode to utilities.
Fixed
  • Don't end HTML comment blocks on a blank line, #​1155.
  • Properly recognize astral chars (surrogates) in delimiter scans for
    emphasis-like markers, #​1072. Big thanks to @​tats-u for his global efforts
    with improving CJK support.
  • Preserve unicode whitespaces when trimm headings/paragraphs, #​1074.
  • More strict entities decode to avoid false positives ;, #​1096.
  • Restore block parser state on fail in lheading rule, #​1131.
Security
  • Fixed poor smartquotes perfomance on > 70k quotes in single block
  • Bumped linkify-it to 5.0.1 with fixed potential perfomance issues.

v14.1.1

Compare Source

Security
  • Fixed regression from v13 in linkify inline rule. Specific patterns could
    cause high CPU use. Thanks to @​ltduc147 for report.

v14.1.0

Compare Source

Changed
  • Updated CM spec compatibility to 0.31.2, #​1009.
Fixed
  • Fixed quadratic complexity when parsing references, #​996.
  • Fixed quadratic output size with pathological user input in tables, #​1000.

v14.0.0

Compare Source

Changed
  • Drop ancient browsers support (use .fromCodePoint and other features).
  • Rewrite to ESM (including all plugins/deps). CJS fallback still available.
    No signatures changed, except markdown-it-emoji plugin.
  • Dropped dist/ folder from repo, build on package publish.
  • Set punicode.js as external dependency.
Fixed
  • Html tokens inside img alt are now rendered as their original text, #​896.
  • Hardbreaks inside img alt are now rendered as newlines.

v13.0.2

Compare Source

Security
  • Fixed crash/infinite loop caused by linkify inline rule, #​957.
Fixed
  • Throw an error if 3rd party plugin doesn't increment line or pos counters
    (previously, markdown-it would likely go into infinite loop instead), #​847.

v13.0.1

Compare Source

Fixed
  • Bumped linkify-it to 4.0.1. That should fix some hangs, caused by wrong
    data, returned from linkify-it.

v13.0.0

Compare Source

Added
  • Added a new token type text_special to store escaped characters, same as text but
    unaffected by replacement plugins (smartquotes, typographer, linkifier, etc.).
  • Added a new rule text_join in core ruler. Text replacement plugins may choose to
    insert themselves before it.
Changed
  • (p) is no longer replaced with § by typographer (conflicts with ℗), #​763.
  • text_collapse rule is renamed to fragments_join.
Fixed
  • Smartquotes, typographic replacements and plain text links can now be escaped
    with backslash (e.g. \(c) or google\.com are no longer replaced).
  • Fixed collision of emphasis and linkifier (so http://example.org/foo._bar_-_baz
    is now a single link, not emphasized). Emails and fuzzy links are not affected by this.

Configuration

📅 Schedule: (UTC)

  • Branch creation
    • At any time (no schedule defined)
  • Automerge
    • At any time (no schedule defined)

🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.

♻ 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 force-pushed the renovate/npm-markdown-it-vulnerability branch from 48529dd to c2ca7a0 Compare July 12, 2026 12:02
@renovate
renovate Bot force-pushed the renovate/npm-markdown-it-vulnerability branch from c2ca7a0 to 67490b6 Compare July 24, 2026 18:59
@renovate
renovate Bot force-pushed the renovate/npm-markdown-it-vulnerability branch from 67490b6 to b50a088 Compare August 11, 2026 21:12
@renovate
renovate Bot force-pushed the renovate/npm-markdown-it-vulnerability branch from b50a088 to 58de487 Compare September 3, 2026 00:52
@renovate
renovate Bot force-pushed the renovate/npm-markdown-it-vulnerability branch from 58de487 to a5a8f00 Compare September 15, 2026 18:53
@renovate
renovate Bot force-pushed the renovate/npm-markdown-it-vulnerability branch from a5a8f00 to 2e0c4c8 Compare October 20, 2026 17:58

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