{"id":"CVE-2026-71430","aliases":["DEBIAN-CVE-2026-71430","EUVD-2026-54159","GHSA-8hcv-x26h-mcgp","UBUNTU-CVE-2026-71430"],"status":"published","title":"node-re2: String.prototype.replace(re2, template) aborts the Node process (uncatchable ToLocalChecked on empty MaybeLocal) when the result exceeds V8's max string length","description":"node-re2 provides RE2 regular expression bindings for Node.js. Prior to version 1.25.1, the WrappedRE2::Replace function built its replacement result and passed it to V8 using ToLocalChecked without checking for the empty MaybeLocal that V8 returns when the resulting string or buffer exceeds V8's maximum string length. When a global replace uses an output amplifying replacement template, the result can grow quadratically with the input size, and once the result exceeds V8's maximum string length, the unchecked ToLocalChecked call causes a fatal, uncatchable process abort instead of a catchable exception. This issue is fixed in version 1.25.1.","severity":{"version":"3.1","score":6.2,"vector":"CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H","level":"medium","assessor":"cna","from":"cve"},"epss":{"day":"2026-11-10","score":0.00157,"percentile":0.04938,"model":"v2026.06.15"},"facts":{"in_kev":false,"known_exploited":false,"public_exploit":false,"fix_available":true},"cwes":["CWE-617"],"published":"2026-08-06T21:19:32Z","modified":"2026-10-28T02:30:34Z","fixes":[{"ecosystem":"npm","package":"re2","fixed_in":["1.25.1"],"sources":["ghsa","osv"]},{"ecosystem":"Debian:14","package":"node-re2","fixed_in":["1.26.1+~cs1.7.0-1"],"sources":["osv"]}],"affected":[{"source":"ghsa","ecosystem":"npm","package":"re2","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"last_affected":"1.25.0"},{"fixed":"1.25.1"}]}]},{"source":"osv","ecosystem":"Debian:12","package":"node-re2","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["1.18.0+~cs2.13.8-1","1.19.2+~cs2.13.13-1","1.20.10+~cs2.13.13-1","1.20.12+~cs2.13.13-1","1.21.4+~cs2.13.13-1","1.21.4+~cs2.13.13-2","1.22.1+~cs1.3.5-1","1.23.3+~cs1.3.5-1","1.25.0+~cs1.6.0-1","1.25.0+~cs1.6.0-2","1.26.1+~cs1.7.0-1","1.26.1+~cs1.8.0-1","1.27.0+~cs1.8.1-1"]},{"source":"osv","ecosystem":"Debian:13","package":"node-re2","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["1.21.4+~cs2.13.13-2","1.22.1+~cs1.3.5-1","1.23.3+~cs1.3.5-1","1.25.0+~cs1.6.0-1","1.25.0+~cs1.6.0-2","1.26.1+~cs1.7.0-1","1.26.1+~cs1.8.0-1","1.27.0+~cs1.8.1-1"]},{"source":"osv","ecosystem":"Debian:14","package":"node-re2","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"1.26.1+~cs1.7.0-1"}]}],"versions":["1.21.4+~cs2.13.13-2","1.22.1+~cs1.3.5-1","1.23.3+~cs1.3.5-1","1.25.0+~cs1.6.0-1","1.25.0+~cs1.6.0-2"]},{"source":"osv","ecosystem":"npm","package":"re2","ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"1.25.1"}]}]},{"source":"osv","ecosystem":"Ubuntu:20.04:LTS","package":"node-re2","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["1.10.4-1","1.10.5-1","1.10.5-1build1"]},{"source":"osv","ecosystem":"Ubuntu:22.04:LTS","package":"node-re2","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["1.15.9+~cs1.8.18-1","1.15.9+~cs1.8.18-2","1.16.0+~cs2.12.8-2","1.17.1+~cs2.12.8-1","1.17.2+~cs2.13.8-1","1.17.3+~cs2.13.8-1"]},{"source":"osv","ecosystem":"Ubuntu:24.04:LTS","package":"node-re2","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["1.18.0+~cs2.13.8-1build1","1.19.2+~cs2.13.13-1build1","1.20.10+~cs2.13.13-1"]},{"source":"osv","ecosystem":"Ubuntu:26.10","package":"node-re2","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["1.23.3+~cs1.3.5-1","1.25.0+~cs1.6.0-2","1.26.1+~cs1.7.0-1"]},{"source":"osv","ecosystem":"Ubuntu:26.04:LTS","package":"node-re2","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["1.22.1+~cs1.3.5-1","1.22.1+~cs1.3.5-1build2","1.22.1+~cs1.3.5-1build3","1.23.3+~cs1.3.5-1"]}],"products":[{"vendor":"uhop","product":"node-re2"}],"references":[{"url":"https://github.com/uhop/node-re2/security/advisories/GHSA-8hcv-x26h-mcgp","tags":["x_refsource_CONFIRM","exploit","advisory","web","report"]},{"url":"https://github.com/advisories/GHSA-8hcv-x26h-mcgp"},{"url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/71xxx/CVE-2026-71430.json","tags":["advisory"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-71430","tags":["advisory"]},{"url":"https://security-tracker.debian.org/tracker/CVE-2026-71430","tags":["advisory"]},{"url":"https://github.com/uhop/node-re2","tags":["package"]},{"url":"https://ubuntu.com/security/CVE-2026-71430","tags":["report"]},{"url":"https://www.cve.org/CVERecord?id=CVE-2026-71430","tags":["report"]}],"sources":{"cve":[{"id":"CVE-2026-71430","title":"node-re2: String.prototype.replace(re2, template) aborts the Node process (uncatchable ToLocalChecked on empty MaybeLocal) when the result exceeds V8's max string length","description":"node-re2 provides RE2 regular expression bindings for Node.js. Prior to version 1.25.1, the WrappedRE2::Replace function built its replacement result and passed it to V8 using ToLocalChecked without checking for the empty MaybeLocal that V8 returns when the resulting string or buffer exceeds V8's maximum string length. When a global replace uses an output amplifying replacement template, the result can grow quadratically with the input size, and once the result exceeds V8's maximum string length, the unchecked ToLocalChecked call causes a fatal, uncatchable process abort instead of a catchable exception. This issue is fixed in version 1.25.1.","status":"published","published":"2026-08-06T21:19:32.77Z","modified":"2026-08-07T17:13:08.294Z","cvss":[{"version":"3.1","score":6.2,"vector":"CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H","level":"medium","assessor":"cna"}],"cwes":["CWE-617"],"updated_at":"2026-11-02T14:19:49.559463Z"}],"euvd":[{"id":"EUVD-2026-54159","description":"node-re2 provides RE2 regular expression bindings for Node.js. Prior to version 1.25.1, the WrappedRE2::Replace function built its replacement result and passed it to V8 using ToLocalChecked without checking for the empty MaybeLocal that V8 returns when the resulting string or buffer exceeds V8's maximum string length. When a global replace uses an output amplifying replacement template, the result can grow quadratically with the input size, and once the result exceeds V8's maximum string length, the unchecked ToLocalChecked call causes a fatal, uncatchable process abort instead of a catchable exception. This issue is fixed in version 1.25.1.","status":"published","published":"2026-08-06T21:19:32Z","modified":"2026-08-07T17:13:08Z","cvss":[{"version":"3.1","score":6.2,"vector":"CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H","level":"medium"}],"updated_at":"2026-11-02T18:02:06.930016Z"}],"ghsa":[{"id":"GHSA-8hcv-x26h-mcgp","title":"node-re2: String.prototype.replace(re2, template) aborts the Node process (uncatchable ToLocalChecked on empty MaybeLocal) when the result exceeds V8's max string length","description":"## Description\n\n`WrappedRE2::Replace` builds the replacement result and hands it to V8 with `.ToLocalChecked()` **without checking for the empty `MaybeLocal`** that V8 returns when the string/buffer exceeds its maximum length:\n\n`lib/replace.cc` (v1.24.1):\n```cpp\n// L553 — Buffer return path\ninfo.GetReturnValue().Set(Nan::CopyBuffer(result.data(), result.size()).ToLocalChecked());\n// L556 — String return path\ninfo.GetReturnValue().Set(Nan::New(result).ToLocalChecked());\n```\n\nWhen a global replace uses an output-amplifying template — `$'` (text after the match) or `` $` `` (text before the match) — the result grows to **O(input²)**. For an input of ~40,000+ identical single-char matches the result exceeds V8's `String::kMaxLength` (~536,870,888 chars on 64-bit). `Nan::New(result)` then returns an **empty `MaybeLocal`**, and the unchecked `.ToLocalChecked()` calls `v8::Utils::ReportApiFailure` → **`FATAL ERROR: v8::ToLocalChecked Empty MaybeLocal`** → `abort()` (SIGABRT).\n\nThis is an **uncatchable** crash: it is not a JavaScript exception, so a surrounding `try/catch` cannot stop it — the entire Node process (or worker) dies.\n\n**The built-in regex engine handles the identical case correctly** by throwing a *catchable* `RangeError: Invalid string length`. node-re2 diverges from that contract and aborts instead.\n\n## Proof of concept\n\n```\nnpm i re2\nnode poc.js\n```\n\n```js\nconst RE2 = require('re2');\n\n// Built-in engine: same case -\u003e CATCHABLE RangeError (correct)\ntry { 'a'.repeat(50000).replace(/a/g, \"$'\"); }\ncatch (e) { console.log('native:', e.constructor.name, e.message); } // RangeError: Invalid string length\n\n// re2: ABORTS the whole process (uncatchable; try/catch does not help)\n'a'.repeat(50000).replace(new RE2('a', 'g'), \"$'\");\n// -\u003e FATAL ERROR: v8::ToLocalChecked Empty MaybeLocal   (process exits 134 / SIGABRT)\n```\n\nObserved (Node v24, clean `npm i re2` → re2@1.24.1): native branch prints `RangeError: Invalid string length`; the re2 branch aborts with `FATAL ERROR: v8::ToLocalChecked Empty MaybeLocal`, stack top `WrappedRE2::Replace`, process exit code **134**.\n\nThreshold matches the mechanism precisely: input of 30,000 chars completes; 40,000 aborts (30000²/2 ≈ 4.5e8 \u003c 5.37e8 max; 40000²/2 ≈ 8e8 \u003e max). `$\u0026`/constant templates and non-global replaces do not amplify and do not crash.\n\n## Impact\n\nA remote, unauthenticated denial of service against any service that runs `String.prototype.replace` / the re2 `[Symbol.replace]` path where either the **replacement template** (containing `$'` or `` $` ``) or the **input size** is attacker-influenced. Because the failure is a native `abort()`, it cannot be contained by `try/catch` or domains — one request takes down the whole process/worker. This is especially impactful for re2's core audience, who adopt it specifically to process untrusted patterns/inputs safely.\n\n## Suggested fix\n\nCheck the `MaybeLocal` before `ToLocalChecked` on both return paths (and the intermediate group-string builds), and throw a catchable `RangeError` to match the built-in engine:\n\n```cpp\nauto maybe = Nan::New(result);\nif (maybe.IsEmpty()) { Nan::ThrowRangeError(\"Invalid string length\"); return; }\ninfo.GetReturnValue().Set(maybe.ToLocalChecked());\n```\n\n(Apply equivalently to the `Nan::CopyBuffer(...)` buffer path at L553 and to the per-group `Nan::New(data, size).ToLocalChecked()` sites used by the replacer-function path.)\n\n## Resolution\n\nResolved in `re2` `1.25.1`. `WrappedRE2::Replace` now checks the returned `MaybeLocal` on every result path and throws a catchable `RangeError: Invalid string length` (matching the built-in engine) instead of aborting the process with an uncatchable `SIGABRT`. No API changes --- upgrade to `re2` \u003e= `1.25.1` via a plain `npm upgrade` to receive the fix.","status":"published","published":"2026-08-06T21:19:36Z","modified":"2026-08-06T21:19:37Z","cvss":[{"version":"3.1","score":6.2,"vector":"CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H","level":"medium"}],"cwes":["CWE-617"],"updated_at":"2026-11-02T14:40:57.999375Z"}],"nvd":[{"id":"CVE-2026-71430","description":"node-re2 provides RE2 regular expression bindings for Node.js. Prior to version 1.25.1, the WrappedRE2::Replace function built its replacement result and passed it to V8 using ToLocalChecked without checking for the empty MaybeLocal that V8 returns when the resulting string or buffer exceeds V8's maximum string length. When a global replace uses an output amplifying replacement template, the result can grow quadratically with the input size, and once the result exceeds V8's maximum string length, the unchecked ToLocalChecked call causes a fatal, uncatchable process abort instead of a catchable exception. This issue is fixed in version 1.25.1.","status":"published","published":"2026-08-06T22:18:29.963Z","modified":"2026-10-24T15:10:00.19Z","cvss":[{"version":"3.1","score":6.2,"vector":"CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H","level":"medium","assessor":"secondary"}],"cwes":["CWE-617"],"updated_at":"2026-11-02T14:12:35.386166Z"}],"osv":[{"id":"CVE-2026-71430","title":"node-re2: String.prototype.replace(re2, template) aborts the Node process (uncatchable ToLocalChecked on empty MaybeLocal) when the result exceeds V8's max string length","description":"node-re2 provides RE2 regular expression bindings for Node.js. Prior to version 1.25.1, the WrappedRE2::Replace function built its replacement result and passed it to V8 using ToLocalChecked without checking for the empty MaybeLocal that V8 returns when the resulting string or buffer exceeds V8's maximum string length. When a global replace uses an output amplifying replacement template, the result can grow quadratically with the input size, and once the result exceeds V8's maximum string length, the unchecked ToLocalChecked call causes a fatal, uncatchable process abort instead of a catchable exception. This issue is fixed in version 1.25.1.","status":"published","published":"2026-08-06T21:19:32.77Z","modified":"2026-10-28T02:30:34.672315214Z","cvss":[{"version":"3.1","score":6.2,"vector":"CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H","level":"medium"}],"cwes":["CWE-617"],"updated_at":"2026-11-02T15:45:15.164658Z"},{"id":"DEBIAN-CVE-2026-71430","description":"node-re2 provides RE2 regular expression bindings for Node.js. Prior to version 1.25.1, the WrappedRE2::Replace function built its replacement result and passed it to V8 using ToLocalChecked without checking for the empty MaybeLocal that V8 returns when the resulting string or buffer exceeds V8's maximum string length. When a global replace uses an output amplifying replacement template, the result can grow quadratically with the input size, and once the result exceeds V8's maximum string length, the unchecked ToLocalChecked call causes a fatal, uncatchable process abort instead of a catchable exception. This issue is fixed in version 1.25.1.","status":"published","published":"2026-08-06T22:18:29.963Z","modified":"2026-10-25T09:47:37.742680296Z","cvss":[{"version":"3.1","score":6.2,"vector":"CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H","level":"medium"}],"updated_at":"2026-11-02T15:46:02.094661Z"},{"id":"GHSA-8hcv-x26h-mcgp","title":"node-re2: String.prototype.replace(re2, template) aborts the Node process (uncatchable ToLocalChecked on empty MaybeLocal) when the result exceeds V8's max string length","description":"## Description\n\n`WrappedRE2::Replace` builds the replacement result and hands it to V8 with `.ToLocalChecked()` **without checking for the empty `MaybeLocal`** that V8 returns when the string/buffer exceeds its maximum length:\n\n`lib/replace.cc` (v1.24.1):\n```cpp\n// L553 — Buffer return path\ninfo.GetReturnValue().Set(Nan::CopyBuffer(result.data(), result.size()).ToLocalChecked());\n// L556 — String return path\ninfo.GetReturnValue().Set(Nan::New(result).ToLocalChecked());\n```\n\nWhen a global replace uses an output-amplifying template — `$'` (text after the match) or `` $` `` (text before the match) — the result grows to **O(input²)**. For an input of ~40,000+ identical single-char matches the result exceeds V8's `String::kMaxLength` (~536,870,888 chars on 64-bit). `Nan::New(result)` then returns an **empty `MaybeLocal`**, and the unchecked `.ToLocalChecked()` calls `v8::Utils::ReportApiFailure` → **`FATAL ERROR: v8::ToLocalChecked Empty MaybeLocal`** → `abort()` (SIGABRT).\n\nThis is an **uncatchable** crash: it is not a JavaScript exception, so a surrounding `try/catch` cannot stop it — the entire Node process (or worker) dies.\n\n**The built-in regex engine handles the identical case correctly** by throwing a *catchable* `RangeError: Invalid string length`. node-re2 diverges from that contract and aborts instead.\n\n## Proof of concept\n\n```\nnpm i re2\nnode poc.js\n```\n\n```js\nconst RE2 = require('re2');\n\n// Built-in engine: same case -\u003e CATCHABLE RangeError (correct)\ntry { 'a'.repeat(50000).replace(/a/g, \"$'\"); }\ncatch (e) { console.log('native:', e.constructor.name, e.message); } // RangeError: Invalid string length\n\n// re2: ABORTS the whole process (uncatchable; try/catch does not help)\n'a'.repeat(50000).replace(new RE2('a', 'g'), \"$'\");\n// -\u003e FATAL ERROR: v8::ToLocalChecked Empty MaybeLocal   (process exits 134 / SIGABRT)\n```\n\nObserved (Node v24, clean `npm i re2` → re2@1.24.1): native branch prints `RangeError: Invalid string length`; the re2 branch aborts with `FATAL ERROR: v8::ToLocalChecked Empty MaybeLocal`, stack top `WrappedRE2::Replace`, process exit code **134**.\n\nThreshold matches the mechanism precisely: input of 30,000 chars completes; 40,000 aborts (30000²/2 ≈ 4.5e8 \u003c 5.37e8 max; 40000²/2 ≈ 8e8 \u003e max). `$\u0026`/constant templates and non-global replaces do not amplify and do not crash.\n\n## Impact\n\nA remote, unauthenticated denial of service against any service that runs `String.prototype.replace` / the re2 `[Symbol.replace]` path where either the **replacement template** (containing `$'` or `` $` ``) or the **input size** is attacker-influenced. Because the failure is a native `abort()`, it cannot be contained by `try/catch` or domains — one request takes down the whole process/worker. This is especially impactful for re2's core audience, who adopt it specifically to process untrusted patterns/inputs safely.\n\n## Suggested fix\n\nCheck the `MaybeLocal` before `ToLocalChecked` on both return paths (and the intermediate group-string builds), and throw a catchable `RangeError` to match the built-in engine:\n\n```cpp\nauto maybe = Nan::New(result);\nif (maybe.IsEmpty()) { Nan::ThrowRangeError(\"Invalid string length\"); return; }\ninfo.GetReturnValue().Set(maybe.ToLocalChecked());\n```\n\n(Apply equivalently to the `Nan::CopyBuffer(...)` buffer path at L553 and to the per-group `Nan::New(data, size).ToLocalChecked()` sites used by the replacer-function path.)\n\n## Resolution\n\nResolved in `re2` `1.25.1`. `WrappedRE2::Replace` now checks the returned `MaybeLocal` on every result path and throws a catchable `RangeError: Invalid string length` (matching the built-in engine) instead of aborting the process with an uncatchable `SIGABRT`. No API changes --- upgrade to `re2` \u003e= `1.25.1` via a plain `npm upgrade` to receive the fix.","status":"published","published":"2026-08-06T21:19:36Z","modified":"2026-08-06T21:40:58.226072Z","cvss":[{"version":"3.1","score":6.2,"vector":"CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H","level":"medium"}],"cwes":["CWE-617"],"updated_at":"2026-11-02T15:46:42.437513Z"},{"id":"UBUNTU-CVE-2026-71430","description":"node-re2 provides RE2 regular expression bindings for Node.js. Prior to version 1.25.1, the WrappedRE2::Replace function built its replacement result and passed it to V8 using ToLocalChecked without checking for the empty MaybeLocal that V8 returns when the resulting string or buffer exceeds V8's maximum string length. When a global replace uses an output amplifying replacement template, the result can grow quadratically with the input size, and once the result exceeds V8's maximum string length, the unchecked ToLocalChecked call causes a fatal, uncatchable process abort instead of a catchable exception. This issue is fixed in version 1.25.1.","status":"published","published":"2026-08-06T22:18:00Z","modified":"2026-10-17T13:00:43.452179554Z","cvss":[{"version":"3.1","score":6.2,"vector":"CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H","level":"medium"}],"updated_at":"2026-11-02T16:57:03.035847Z"}]}}
