GHSA-8HCV-X26H-MCGP

Vulnerability from github – Published: 2026-08-06 21:19 – Updated: 2026-08-06 21:19
VLAI
Summary
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
Details

Description

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:

lib/replace.cc (v1.24.1):

// L553 — Buffer return path
info.GetReturnValue().Set(Nan::CopyBuffer(result.data(), result.size()).ToLocalChecked());
// L556 — String return path
info.GetReturnValue().Set(Nan::New(result).ToLocalChecked());

When 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).

This 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.

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.

Proof of concept

npm i re2
node poc.js
const RE2 = require('re2');

// Built-in engine: same case -> CATCHABLE RangeError (correct)
try { 'a'.repeat(50000).replace(/a/g, "$'"); }
catch (e) { console.log('native:', e.constructor.name, e.message); } // RangeError: Invalid string length

// re2: ABORTS the whole process (uncatchable; try/catch does not help)
'a'.repeat(50000).replace(new RE2('a', 'g'), "$'");
// -> FATAL ERROR: v8::ToLocalChecked Empty MaybeLocal   (process exits 134 / SIGABRT)

Observed (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.

Threshold matches the mechanism precisely: input of 30,000 chars completes; 40,000 aborts (30000²/2 ≈ 4.5e8 < 5.37e8 max; 40000²/2 ≈ 8e8 > max). $&/constant templates and non-global replaces do not amplify and do not crash.

Impact

A 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.

Suggested fix

Check 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:

auto maybe = Nan::New(result);
if (maybe.IsEmpty()) { Nan::ThrowRangeError("Invalid string length"); return; }
info.GetReturnValue().Set(maybe.ToLocalChecked());

(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.)

Resolution

Resolved 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 >= 1.25.1 via a plain npm upgrade to receive the fix.

Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 1.25.0"
      },
      "package": {
        "ecosystem": "npm",
        "name": "re2"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "1.25.1"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-71430"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-617"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-08-06T21:19:36Z",
    "nvd_published_at": null,
    "severity": "MODERATE"
  },
  "details": "## 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 \u2014 Buffer return path\ninfo.GetReturnValue().Set(Nan::CopyBuffer(result.data(), result.size()).ToLocalChecked());\n// L556 \u2014 String return path\ninfo.GetReturnValue().Set(Nan::New(result).ToLocalChecked());\n```\n\nWhen a global replace uses an output-amplifying template \u2014 `$\u0027` (text after the match) or `` $` `` (text before the match) \u2014 the result grows to **O(input\u00b2)**. For an input of ~40,000+ identical single-char matches the result exceeds V8\u0027s `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` \u2192 **`FATAL ERROR: v8::ToLocalChecked Empty MaybeLocal`** \u2192 `abort()` (SIGABRT).\n\nThis is an **uncatchable** crash: it is not a JavaScript exception, so a surrounding `try/catch` cannot stop it \u2014 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(\u0027re2\u0027);\n\n// Built-in engine: same case -\u003e CATCHABLE RangeError (correct)\ntry { \u0027a\u0027.repeat(50000).replace(/a/g, \"$\u0027\"); }\ncatch (e) { console.log(\u0027native:\u0027, e.constructor.name, e.message); } // RangeError: Invalid string length\n\n// re2: ABORTS the whole process (uncatchable; try/catch does not help)\n\u0027a\u0027.repeat(50000).replace(new RE2(\u0027a\u0027, \u0027g\u0027), \"$\u0027\");\n// -\u003e FATAL ERROR: v8::ToLocalChecked Empty MaybeLocal   (process exits 134 / SIGABRT)\n```\n\nObserved (Node v24, clean `npm i re2` \u2192 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\u00b2/2 \u2248 4.5e8 \u003c 5.37e8 max; 40000\u00b2/2 \u2248 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 `$\u0027` or `` $` ``) or the **input size** is attacker-influenced. Because the failure is a native `abort()`, it cannot be contained by `try/catch` or domains \u2014 one request takes down the whole process/worker. This is especially impactful for re2\u0027s 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.",
  "id": "GHSA-8hcv-x26h-mcgp",
  "modified": "2026-08-06T21:19:36Z",
  "published": "2026-08-06T21:19:36Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/uhop/node-re2/security/advisories/GHSA-8hcv-x26h-mcgp"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/uhop/node-re2"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H",
      "type": "CVSS_V3"
    }
  ],
  "summary": "node-re2: String.prototype.replace(re2, template) aborts the Node process (uncatchable ToLocalChecked on empty MaybeLocal) when the result exceeds V8\u0027s max string length"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.

Loading…

Loading…

Loading…

Related by attack behaviour

Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.


Loading…