{"id":"GHSA-253c-mchw-3w2r","kind":"vulnerability","aliases":["GHSA-253c-mchw-3w2r"],"related":[],"title":"markdown-it linkify: true has two quadratic paths, so a few hundred KB of markdown blocks the event loop for tens of seconds","description":"## Summary\n\nTwo 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.\n\n`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.\n\n`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.\n\n## Proof of concept\n\nClean install of 15.0.0 from npm, `new MarkdownIt({ linkify: true }).render(payload)`, Node 24.\n\n| N | 5,000 | 10,000 | 20,000 | 40,000 |\n|---|---|---|---|---|\n| `'a@b.co\\n'.repeat(N)`, 34KB to 273KB | 1.1s | 4.7s | 19.6s | 89s |\n| `'a://'.repeat(N)`, 20KB to 156KB | 0.25s | 0.76s | 3.1s | 15.0s |\n\nDoubling the input roughly quadruples the time in both. With `linkify: false` the same inputs run in 25 to 131ms and stay flat.\n\nControls 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.\n\nOrdinary prose does it too. `'ping a@b.co ok\\n'.repeat(20000)` is 293KB and takes 31s.\n\n## Caveat\n\n`linkify` is off by default, so this only reaches apps that turn it on.\n\n## Impact\n\nAvailability 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.","summary":"markdown-it with linkify enabled has two quadratic-time code paths, so a few hundred KB of crafted markdown can block the Node.js event loop for tens of seconds. The attacker gets denial of service only; nothing is read, written or executed.","severity":"medium","cvss":null,"epss":null,"exploited":false,"exploited_at":null,"products":[{"name":"markdown-it","fixed":["15.0.1"],"affected":"= 15.0.0","ecosystem":"npm"},{"name":"markdown-it","fixed":["14.3.1"],"affected":"< 14.3.1","ecosystem":"npm"}],"ecosystems":["npm"],"tags":["cwe-400","cwe-407","markdown-it","node.js","package","user-supplied-markdown","web-apps"],"not_affected_if":["default-config","linkify-disabled","no-untrusted-markdown"],"how_to_check":"Run `npm ls markdown-it` and look for 15.0.0 or any version below 14.3.1 in the tree.\nCheck whether the app sets `linkify: true` in its MarkdownIt constructor; with the default `linkify: false` the advisory does not apply.","fix":"Upgrade markdown-it to 15.0.1, or to 14.3.1 if you are on the 14.x line.\nIf you cannot upgrade, keep linkify off or cap the size of user-supplied markdown before rendering.","status":"open","published_at":"2026-09-29T17:57:56+00:00","first_seen_at":"2026-10-23T19:11:31+00:00","triaged_at":"2026-10-23T20:20:16+00:00","updated_at":"2026-10-23T20:20:16+00:00","category":"package","confidence":0.85,"triage":{"at":"2026-10-23T20:20:16Z","model":"claude-haiku-5-5","output":{"fix":"Upgrade markdown-it to 15.0.1, or to 14.3.1 if you are on the 14.x line.\nIf you cannot upgrade, keep linkify off or cap the size of user-supplied markdown before rendering.","summary":"markdown-it with linkify enabled has two quadratic-time code paths, so a few hundred KB of crafted markdown can block the Node.js event loop for tens of seconds. The attacker gets denial of service only; nothing is read, written or executed.","category":"package","who_cares":["node.js","markdown-it","web-apps","user-supplied-markdown"],"confidence":0.85,"analyst_note":"","how_to_check":"Run `npm ls markdown-it` and look for 15.0.0 or any version below 14.3.1 in the tree.\nCheck whether the app sets `linkify: true` in its MarkdownIt constructor; with the default `linkify: false` the advisory does not apply.","needs_analyst":false,"not_affected_if":["default-config","linkify-disabled","no-untrusted-markdown"],"severity_if_unknown":"medium"},"cost_usd":0.000474,"input_tokens":2854,"output_tokens":378,"severity_before":"medium"},"analyst_status":null,"analyst_note":null,"search":"'-400':42B '-407':44B '0.25':279C '0.59':329C '0.64':339C '0.76':281C '000':253C,255C,257C,259C '1.1':267C '10':254C '11.7':383C '131ms':306C '15.0':285C '15.0.0':240C '156kb':278C '15s':387C '19.6':271C '20':256C '20000':318C,398C '20kb':276C '24':250C '25':304C '253c':27A,32A '273kb':266C '293kb':400C '2n':163C,172C '3.1':283C '31s':403C '34kb':264C '3w2r':29A,34A '4.7':269C '40':258C '42ms':350C '5':252C '89s':273C 'a@b.co':260C,327C,335C,394C 'account':381C 'actual':220C 'app':56B,414C 'array':146C,173C 'arrayreplaceat':134C 'arriv':193C 'attack':89B 'avail':420C 'block':17A,79B 'broken':155C,344C 'call':133C,369C 'caveat':404C 'children':145C,164C,355C 'clean':237C 'code':68B,119C 'concept':236C 'consum':185C 'control':310C 'core':432C 'craft':76B 'cwe':41B,43B 'default':409C 'degrad':443C 'denial':91B 'doubl':287C 'email':177C,321C,332C 'enabl':62B 'event':19A,82B 'everi':208C,230C,320C 'execut':100B,452C 'fair':427C 'fals':298C 'first':190C,313C 'flat':309C 'get':90B,167C,184C,214C 'ghsa':26A,31A 'ghsa-253c-mchw-3w2r':25A,30A 'grow':228C,373C 'handl':110C 'hundr':13A,73B,424C 'impact':419C 'independ':103C 'inlin':159C,188C 'input':289C,301C 'instal':238C 'involv':380C 'it':37A,40A 'kb':14A,74B,425C 'leav':226C 'less':176C 'line':156C,345C 'linear':128C,201C 'link':183C,218C 'linkifi':4A,61B,108C,124C,137C,245C,297C,348C,360C,405C 'linkify-it':123C 'loop':20A,83B 'lot':233C 'mani':354C,357C 'markdown':2A,16A,36A,39A,46B,53B,58B,77B,115C,378C,429C 'markdown-it':1A,35A,38A,45B,57B,114C,377C 'markdownit':244C 'mchw':28A,33A 'n':152C,168C,251C,261C,263C,275C,317C,336C,337C,396C 'need':353C 'never':192C 'new':243C 'node':249C 'node.js':48B,81B 'noth':95B,347C,447C 'npm':242C 'ok':395C 'one':158C,314C,331C,431C 'ordinari':388C,428C 'packag':49B 'paragraph':150C,334C 'path':9A,69B,105C 'payload':131C,248C 'per':136C,333C 'pin':430C 'ping':393C 'prefix':374C 'produc':221C 'proof':234C 'prose':389C 'put':319C 'quadrat':8A,66B,104C,441C 'quadratic-tim':65B 'quadrupl':291C 'quick':444C 'rather':120C 're':206C,368C 'reach':180C,413C 'read':97B,449C 'rebuild':142C,169C 'render':247C 'repeat':262C,274C,397C 'replay':364C 'rescan':231C 'rough':290C 'rule':189C 'run':203C,302C 'schema':175C 'schema-less':174C 'scheme':205C,225C,367C 'second':24A,87B,363C,436C 'servic':93B 'singl':324C 'size':446C 'soft':154C,343C 'soft-broken':153C,342C 'sourc':211C 'src/rules_core/linkify.ts':132C 'src/rules_inline/linkify.ts':202C 'state.pending':212C 'state.pending.match':204C 'stay':127C,200C,308C 'summari':101C 'suppli':52B 'take':328C,338C,349C,402C 'ten':22A,85B,434C 'text':138C,196C,325C 'time':67B,148C,293C 'token':139C,160C,197C,326C 'true':5A,109C,246C 'truncat':215C 'turn':416C 'two':7A,64B,102C 'unregist':224C 'user':51B 'user-supplied-markdown':50B 'web':55B 'web-app':54B 'whole':144C 'written':98B,450C","nicknames":[],"mitigations":[],"who_affected":null,"tier":null,"verified":{},"analyst_revisit_at":null,"first_public_at":"2026-09-29T17:57:56+00:00","kev":null,"cvss_vector":null,"cvss_by":null,"poc_count":0,"sources":[{"source":"ghsa","external_id":"GHSA-253c-mchw-3w2r","url":"https://github.com/advisories/GHSA-253c-mchw-3w2r","severity":"medium","published_at":"2026-09-29T17:57:56+00:00","modified_at":"2026-10-22T20:35:11+00:00","first_seen_at":"2026-10-23T19:11:31+00:00","fetched_at":"2026-10-23T19:11:31+00:00","note":null}],"documents":[],"notice":"Free to read at a human pace. An API with keys and an MCP server are in preparation; early access: hello@infrastack.ai"}