GHSA-6HXR-MR5R-9836

Vulnerability from github – Published: 2026-07-31 16:53 – Updated: 2026-07-31 16:53
VLAI
Summary
re2: Global `String.prototype.match` with an empty-matchable pattern never advances → infinite loop with unbounded native memory growth (DoS)
Details

Summary

String.prototype.match with a global RE2 collects all matches in a native loop that advances the cursor by the match length. A zero-width (empty) match has length 0, so the cursor never advances: the same empty match is found forever and appended to an ever-growing native vector. Any pattern that can match the empty string (a*, b?, x{0,3}, (a)|, (?:), …) therefore causes an infinite loop with unbounded memory growth. The call is synchronous native code, so it blocks the entire event loop and cannot be interrupted by try/catch, AbortController, --max-old-space-size, or timers — the process must be killed externally. This diverges from the built-in engine, where 'xxxx'.match(/a*/g) returns a finite array.

Root cause

// lib/match.cc:44 — global branch of WrappedRE2::Match
while (re2->regexp.Match(str, byteIndex, str.size, anchor, &match, 1)) {
    groups.push_back(match);
    byteIndex = match.data() - str.data + match.size();   // += 0 for a zero-width match
}

When match.size() == 0, byteIndex is unchanged, so the next iteration matches the same empty position again; groups grows without bound. The other iteration paths already guard this: lib/split.cc:50-55 advances by getUtf8CharSize on an empty match, and exec advances lastIndex. Only this global Match loop is missing the guard.

Proof of concept

const RE2 = require('re2');
'x'.match(new RE2('a*', 'g'));   // never returns; grows memory until OOM
// also: 'b?', 'x{0,3}', '(a)|', 'c*d*', '(?:)'; empty subject '' triggers it too

Compare with the built-in engine, which terminates:

'xxxx'.match(/a*/g);   // -> ["", "", "", "", ""]

Measured on a clean npm install re2@1.25.1 (latest), stock prebuilt binary: resident memory grew ~550 MB → 2.3 GB in ~3 seconds at 100% CPU, and the process had to be SIGKILLed externally.

Impact

Denial of service. Reachable remotely and without authentication wherever an application runs a global RE2 through String.prototype.match and either the pattern or the subject is attacker-influenced — e.g. a user-supplied regular expression, or a fixed empty-matchable pattern applied to user input. Because the loop blocks the event loop and exhausts memory in seconds, a single request can wedge a worker and, via memory exhaustion, affect the whole host.

Suggested fix

Mirror the empty-match handling already present in split.cc: when the match is zero-width, advance the cursor by one code point.

// lib/match.cc, inside the global while-loop
groups.push_back(match);
size_t off = match.data() - str.data;
if (match.size()) {
    byteIndex = off + match.size();
} else {
    byteIndex = off + (off < str.size ? getUtf8CharSize(str.data[off]) : 1);
}

Resolution

Fixed in re2 1.25.2.

The global match loop in lib/match.cc now advances the cursor by one Unicode code point when a match is zero-width, so a pattern that can match the empty string terminates with a finite result identical to the built-in engine ('xxxx'.match(/a*/g) returns five empty strings). This mirrors the guard already present in split.

Remediation: upgrade to re2@1.25.2 or later.

Workaround (if you cannot upgrade): do not run a global RE2 through String.prototype.match when the pattern is attacker-influenced or can match the empty string. Iterate with matchAll/exec, or use the non-global form; both already advanced the cursor correctly.

Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 1.25.1"
      },
      "package": {
        "ecosystem": "npm",
        "name": "re2"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "1.25.2"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-68499"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-835"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-07-31T16:53:08Z",
    "nvd_published_at": "2026-07-30T21:18:12Z",
    "severity": "MODERATE"
  },
  "details": "## Summary\n\n`String.prototype.match` with a **global** `RE2` collects all matches in a native loop that advances the cursor by the match length. A **zero-width (empty) match** has length 0, so the cursor never advances: the same empty match is found forever and appended to an ever-growing native vector. Any pattern that can match the empty string (`a*`, `b?`, `x{0,3}`, `(a)|`, `(?:)`, \u2026) therefore causes an infinite loop with unbounded memory growth. The call is synchronous native code, so it blocks the entire event loop and cannot be interrupted by `try/catch`, `AbortController`, `--max-old-space-size`, or timers \u2014 the process must be killed externally. This diverges from the built-in engine, where `\u0027xxxx\u0027.match(/a*/g)` returns a finite array.\n\n## Root cause\n\n```cpp\n// lib/match.cc:44 \u2014 global branch of WrappedRE2::Match\nwhile (re2-\u003eregexp.Match(str, byteIndex, str.size, anchor, \u0026match, 1)) {\n    groups.push_back(match);\n    byteIndex = match.data() - str.data + match.size();   // += 0 for a zero-width match\n}\n```\n\nWhen `match.size() == 0`, `byteIndex` is unchanged, so the next iteration matches the same empty position again; `groups` grows without bound. The other iteration paths already guard this: `lib/split.cc:50-55` advances by `getUtf8CharSize` on an empty match, and `exec` advances `lastIndex`. Only this global `Match` loop is missing the guard.\n\n## Proof of concept\n\n```js\nconst RE2 = require(\u0027re2\u0027);\n\u0027x\u0027.match(new RE2(\u0027a*\u0027, \u0027g\u0027));   // never returns; grows memory until OOM\n// also: \u0027b?\u0027, \u0027x{0,3}\u0027, \u0027(a)|\u0027, \u0027c*d*\u0027, \u0027(?:)\u0027; empty subject \u0027\u0027 triggers it too\n```\n\nCompare with the built-in engine, which terminates:\n\n```js\n\u0027xxxx\u0027.match(/a*/g);   // -\u003e [\"\", \"\", \"\", \"\", \"\"]\n```\n\nMeasured on a clean `npm install re2@1.25.1` (latest), stock prebuilt binary: resident memory grew **~550 MB \u2192 2.3 GB in ~3 seconds** at 100% CPU, and the process had to be `SIGKILL`ed externally.\n\n## Impact\n\nDenial of service. Reachable remotely and without authentication wherever an application runs a **global** `RE2` through `String.prototype.match` and either the pattern or the subject is attacker-influenced \u2014 e.g. a user-supplied regular expression, or a fixed empty-matchable pattern applied to user input. Because the loop blocks the event loop and exhausts memory in seconds, a single request can wedge a worker and, via memory exhaustion, affect the whole host.\n\n## Suggested fix\n\nMirror the empty-match handling already present in `split.cc`: when the match is zero-width, advance the cursor by one code point.\n\n```cpp\n// lib/match.cc, inside the global while-loop\ngroups.push_back(match);\nsize_t off = match.data() - str.data;\nif (match.size()) {\n    byteIndex = off + match.size();\n} else {\n    byteIndex = off + (off \u003c str.size ? getUtf8CharSize(str.data[off]) : 1);\n}\n```\n\n## Resolution\n\nFixed in re2 1.25.2.\n\nThe global match loop in `lib/match.cc` now advances the cursor by one Unicode\ncode point when a match is zero-width, so a pattern that can match the empty\nstring terminates with a finite result identical to the built-in engine\n(`\u0027xxxx\u0027.match(/a*/g)` returns five empty strings). This mirrors the guard\nalready present in `split`.\n\n**Remediation:** upgrade to `re2@1.25.2` or later.\n\n**Workaround** (if you cannot upgrade): do not run a global `RE2` through\n`String.prototype.match` when the pattern is attacker-influenced or can match\nthe empty string. Iterate with `matchAll`/`exec`, or use the non-global form;\nboth already advanced the cursor correctly.",
  "id": "GHSA-6hxr-mr5r-9836",
  "modified": "2026-07-31T16:53:08Z",
  "published": "2026-07-31T16:53:08Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/uhop/node-re2/security/advisories/GHSA-6hxr-mr5r-9836"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-68499"
    },
    {
      "type": "WEB",
      "url": "https://github.com/uhop/node-re2/commit/56293de4fc0914d7bc35f92e98de25b0d9bb417d"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/uhop/node-re2"
    },
    {
      "type": "WEB",
      "url": "https://github.com/uhop/node-re2/releases/tag/1.25.2"
    }
  ],
  "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": "re2: Global `String.prototype.match` with an empty-matchable pattern never advances \u2192 infinite loop with unbounded native memory growth (DoS)"
}



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…

Detection rules are retrieved from Rulezet.

Loading…

Loading…