RE2JS: Quadratic-time denial of service in re2js lookbehind matching
CVE-2026-116795 · GHSA-gg6h-894w-4m9c · published 28 Oct 2026
How serious is it
- Severity: high, CVSS score 7.5 out of 10.
- No exploit prediction (EPSS) score yet.
- Not on the CISA list of exploited vulnerabilities.
- No public exploit in Exploit-DB.
Is your app affected by CVE-2026-116795?
Paste this prompt into Claude Code, Cursor or any AI coding tool: it runs a free Shippable scan of your project and fixes what it finds. Built with Lovable, Bolt or v0? Scan your live app. Or upload your package file.
Who is affected
re2js(npm) >= 2.2.0, < 2.8.7
How to check
- Run
npm ls re2jsin your project to see the installed version, including copies pulled in by other packages. - Or paste your lockfile into the checker: it lists every package with a known vulnerability, this one included.
- Or run
npx shippable-dev scan localhost:3000in your project folder. It reads your lockfile and flags this vulnerability along with every other known one.
How to fix
Copies a prompt for Claude Code, Cursor or Codex: it checks your lockfiles for this and upgrades it.
- Upgrade
re2jsto 2.8.7 or later, commit the lockfile and redeploy. - If a vulnerable version was live, check your logs and rotate the secrets that server could read.
The advisory
### Summary
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**.
### Description
#### Root cause
In `build/index.cjs`, `Machine.match`:
```js
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.
#### Isolating control
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.
#### Impact
- `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.
- Realistic affected code paths: log/text processing that tokenizes or
redacts using a lookbehind-based pattern (`matchAll`, `split`,
`replaceAll` are the natural APIs for exactly this), applied to
attacker-influenced text (user input, uploaded logs, scraped content).
- A single ~16–32 KB request pins a CPU core for tens of seconds to minutes;
Node.js being single-threaded, this freezes the whole worker process for
the duration. No authentication or special pattern knowledge is required
from the attacker — ordinary alternating text (`"abababab..."`) triggers it.
#### Reproduction (copy-paste runnable)
create file poc.js
```js
'use strict';
// PoC: quadratic-time DoS in re2js's lookbehind matching (opt-in LOOKBEHINDS flag).
// Usage:
// npm pack re2js@2.8.6 --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
```sh
npm pack re2js@2.8.6 --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.
Check your whole app
Dependencies are only half of it. Free and read-only: before you push, run npx shippable-dev scan localhost:3000 in your project folder, no account needed. For your live app, you prove it's yours with a one-line tag first.
References
- github.com/le0pard/re2js/security/advisories/GHSA-gg6h-894w-4m9c
- nvd.nist.gov/vuln/detail/CVE-2026-116795
- github.com/le0pard/re2js/pull/59
- github.com/le0pard/re2js/commit/dfdd947785dfa113bdc97416375c246a39458395
- github.com/le0pard/re2js/releases/tag/2.8.7
- github.com/advisories/GHSA-gg6h-894w-4m9c
- github.com/le0pard/re2js
Merged from GitHub Advisory Database, OSV. Last updated 29 Oct 2026. All vulnerabilities · RSS · JSON