Skip to content

Bump markdown-it from 14.3.0 to 14.3.3 - #270

Open
superbot-helper wants to merge 1 commit into
modem-dev:mainfrom
superbot-helper:security/bump-markdown-it-14.3.3
Open

superbot-helper wants to merge 1 commit into
modem-dev:mainfrom
superbot-helper:security/bump-markdown-it-14.3.3

Conversation

@superbot-helper

Copy link
Copy Markdown

Updates markdown-it from 14.3.0 to 14.3.3.

Fixes GHSA-gfr2-rv44-r23c — markdown-it's block reference rule appends continuation lines to a cons-string with str += lineContent and then keeps indexing the accumulated string with str.charCodeAt(pos) and str.slice(...); V8 flattens the cons-string on the first index or slice after each append, copying everything collected so far, so one link reference definition whose label or title is never closed costs O(n^2) in the number of lines, and the work happens before the rule decides the definition is invalid. The route needs no option: html:false, linkify:true and the highlight callback in server/richRender.ts:185-189 leave the reference rule and its default block configuration untouched: a single GET /p/:id?surface=0&ver=1 through this repository's own server/app.ts Hono app and server/richRender.ts renderMarkdown, carrying a 400,009-byte markdown surface that is one unclosed link reference definition, took a median 5122.66 ms (4441.30-5506.92 ms) on markdown-it 14.3.0, while the same 400,009 bytes with the leading "[" replaced by "x" took a median 95.81 ms (89.05-102.94 ms) on the same tree and the crafted surface took a median 129.05 ms (114.20-169.23 ms) with 14.3.3.

What this changes:

  • package.json — package.json:108: "markdown-it": "^14.2.0" -> "^14.3.3" (the only dependency change; the installed version moves 14.3.0 -> 14.3.3).
  • package-lock.json — package-lock.json: root packages[""].dependencies["markdown-it"] and node_modules/markdown-it move to ^14.3.3 / 14.3.3 with the 14.3.3 tarball and integrity hash; no other package entry changes.

How to verify:

npm ci --ignore-scripts in each tree installs markdown-it 14.3.0 at base and 14.3.3 with this change, read from node_modules/markdown-it/package.json
POST /api/posts then GET /p/:id?surface=0&ver=1 through the tree's own server/app.ts createApp and server/richRender.ts renderMarkdown with a 400,009-byte unclosed link reference definition -> 4441.30-5506.92 ms at base, 114.20-169.23 ms with this change
the same 400,009 bytes with the leading "[" replaced by "x" -> 89.05-102.94 ms on the base tree and 89.05-150.97 ms with this change (the control)
a 16-byte "hello **world**" document through the same route -> 82.31-341.58 ms at base and 73.64-196.75 ms with this change (the baseline)

Measured before and after, same code path, same payload

Both trees are this repository at a3b18af5a77f40889ec1f06359a277ccc61310ff; the only difference is package.json and package-lock.json, which move markdown-it 14.3.0 to 14.3.3. Each row publishes one markdown surface through this repository's own POST /api/posts and then times one GET /p/:id?surface=0&ver=1 through the same server/app.ts Hono app and server/richRender.ts renderMarkdown; each sample runs in a fresh Node process and the first render in a process pays a warmup that every row and both trees pay. Nine samples per row per tree, three runs, alternating trees sample by sample.

markdown surface base markdown-it 14.3.0 this PR markdown-it 14.3.3
crafted: one unclosed link reference definition, [a]: /u " + 200,000 x\n lines (400,009 B) 5122.66 ms median (4441.30-5506.92), HTTP 200 129.05 ms median (114.20-169.23), HTTP 200
control: the same payload with the leading [ replaced by x (400,009 B) 95.81 ms median (89.05-102.94), HTTP 200 95.53 ms median (89.05-150.97), HTTP 200
baseline: hello **world** (16 B) 94.34 ms median (82.31-341.58), HTTP 200 96.03 ms median (73.64-196.75), HTTP 200

The control carries the same 400,009 bytes, the same 200,001 lines and the same link-title text as the crafted payload: only the [ that opens the block reference definition becomes x. Both rows render to the same 407,470-byte page. On the base tree the crafted row is about 53x its control; with 14.3.3 the crafted row costs 129.05 ms against 95.53 ms for its control.

Absolute timings depend on machine load and on the ~80 ms first-render warmup each fresh process pays before the timed request (visible in the baseline row); the ordering between the crafted row and its same-byte-count control does not.

Where the cost is

server/app.ts:1213 mounts POST /api/posts, which validates and stores a markdown surface whose body is the request JSON (server/postSurfaces.ts, 2 MiB surface cap). server/app.ts:1672 mounts GET /p/:id (and :1671 the legacy alias GET /s/:id) to renderPostPage (defined at :1568); for a surface of kind markdown it calls renderMarkdown(surface) at server/app.ts:1655-1656. server/richRender.ts:176 renderMarkdown constructs new MarkdownIt({ html: false, linkify: true, highlight }) at :185-189 and calls md.render(src) at :199.
Inside markdown-it 14.3.0, src/rules_block/reference.ts pulls continuation lines with str += lineContent (lines 74, 83, 100, 133 and 150) and then keeps indexing the accumulated string with str.charCodeAt(pos) in the label and whitespace loops and str.slice(...) in parseLinkTitle; V8 represents += as a cons-string and the next index or slice flattens it, copying everything collected so far, so one definition spanning n lines costs O(n^2). The work happens before the rule decides the definition is invalid. the advisory's own isolation shows md.disable('reference') makes the same input linear, and in this packet's control the same bytes without the leading [ never enter the rule at all.
The fix in 14.3.3 (48fbc46, d99f488) tracks positions in state.src instead of building the string and bounds the label and title scan, so the crafted 400,009 bytes render in the same time as their control.

Scope

package.json: markdown-it ^14.2.0 -> ^14.3.3, one line.
package-lock.json: the root dependency entry and the node_modules/markdown-it entry move 14.3.0 -> 14.3.3 with the registry tarball and integrity hash for 14.3.3; no other lock entry changes.
No application source, template, config or test changes. The renderer API used here (renderMarkdown, new MarkdownIt) is unchanged between the two patch releases.

What this does not claim

Only the 14.3.0 -> 14.3.3 move was measured; the advisory also covers 14.1.0..14.3.2 and 15.0.0..15.0.2, and trees on those versions were not measured here.
The measurements are single-request costs in milliseconds through the application's own in-process Hono app.fetch pipeline (the same createApp, SqlStore and renderMarkdown modules the Node server runs), not a live HTTP server, a Worker deployment or a concurrent-request throughput figure. The ceiling is one request's wall-clock time inside one Node process: the crafted row is ~5.1 s at base and ~0.13 s with this change, absorbed by that process's event loop.
Absolute timings depend on machine load; only the ordering between a crafted row and its same-byte-count control is claimed.
The two sibling markdown-it advisories that also cover 14.3.0 (GHSA-r7fv-28h4-cvq7 smartquotes and GHSA-vfgh-jcmw-g37q inline HTML) need typographer: true and html: true, which this renderer does not enable, so this packet does not claim to exercise them.

Crafted and same-byte control markdown were measured through this repository's own /p/:id route: 5.1 s on markdown-it 14.3.0 vs 96 ms for the control, 0.13 s on 14.3.3.

Automated security patch from Superbot.

GHSA-gfr2-rv44-r23c (fixed in markdown-it 14.3.3 and 15.0.3): a link reference
definition whose label or title is never closed is O(n^2) with default options.
src/rules_block/reference.ts appends continuation lines with str += lineContent
and then keeps indexing the accumulated string with str.charCodeAt and
str.slice, so V8 flattens the cons-string once per line and one definition
spanning n lines costs O(n^2); the work happens before the rule decides the
definition is invalid.

This repository renders user markdown server-side: GET /p/:id?surface=N runs
server/app.ts renderPostPage -> server/richRender.ts renderMarkdown -> md.render
(src) with default block rules. Through that route, a 400,009-byte unclosed
link reference definition ("[a]: /u \"" + 200000 "x\n" lines) was measured on
both trees, three runs of three samples, alternating trees: a median
5,122.7 ms at markdown-it 14.3.0 against 95.8 ms for the same
400,009 bytes with the leading "[" replaced by "x" (the same-length control),
and a median 129.1 ms at 14.3.3.

Absolute timings depend on machine load; the ordering between the crafted row
and its same-byte-count control does not.

package.json and package-lock.json move the single pin ^14.2.0 -> ^14.3.3 and
its lock entry 14.3.0 -> 14.3.3; no application source changes.
@socket-security

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Updatedmarkdown-it@​14.3.0 ⏵ 14.3.386 +1100 +24100 +191100

View full report

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.

1 participant