
Cloud Vulnerability DB
A community-led vulnerabilities database
RE2.prototype.split (and String.prototype.split with an RE2 separator) runs in O(n²) time when the leftmost match is zero-width and lies ahead of the cursor. This happens with \b and \B, and with ^/$ under the m flag. Each such match makes the loop emit the single character at the cursor, step forward one character, and search again from there, which finds the same distant match again. A 200,000-character subject (~600 KB of ordinary CJK text) blocks the event loop for about 81 seconds. The built-in engine takes 1 ms. The output is also wrong: one piece per character instead of one piece per boundary.
This is the same empty-match iteration family as CVE-2026-68499 (GHSA-6hxr-mr5r-9836). That advisory named split's empty-match guard as the safe model for its fix, but that guard is the root cause here.
// lib/split.cc (1.27.0), WrappedRE2::Split
while (byteIndex < str.size && re2->regexp.Match(str, byteIndex, str.size, RE2::UNANCHORED, &groups[0], groups.size()))
{
if (match.size()) { /* split at the match */ }
else
{
// zero-width match: push ONE character at byteIndex, advance ONE character,
// regardless of where the match actually is
size_t sym_size = getUtf8CharSize(str.data[byteIndex], str.size - byteIndex);
pieces.push_back(re2::StringPiece(str.data + byteIndex, sym_size));
byteIndex += sym_size;
}
}When the empty match is at position m > byteIndex, the match position is ignored. Every iteration rescans [byteIndex, m], so a subject of n characters with one boundary at the end costs about n²/2 character steps. RE2's \b is ASCII-only, so any run of non-ASCII text (Chinese, Japanese, Cyrillic, emoji), whitespace or punctuation is a run with no boundary inside it.
const RE2 = require('re2'); // 1.27.0
const s = '中文文本测试内容'.repeat(25000) + 'a'; // 200,001 chars
console.time('re2'); s.split(new RE2('\\b')); console.timeEnd('re2'); // ~81 s
console.time('native'); s.split(/\b/u); console.timeEnd('native'); // ~1 ms
' a'.split(new RE2('\\b')); // [" ", " ", "a"] native: [" ", "a"]
'abc'.split(new RE2('$', 'm')); // ["a", "b", "c"] native: ["abc"]Measured on a clean npm install re2@1.27.0 (stock prebuilt, Node 26, x86-64 Linux):
| subject (CJK + "a") | re2 1.27.0 | built-in |
|---|---|---|
| 25,000 chars | 1.3 s | 0 ms |
| 50,000 | 5.0 s | 0 ms |
| 100,000 | 20.4 s | 0 ms |
| 200,000 | 80.9 s | 1 ms |
Time grows 4× per doubling. Other triggers: ' '.repeat(200000) + 'a' with \b (26.7 s), and 'x'.repeat(100000) + '\n' with new RE2('^', 'm') (6.9 s). | ||
| <img width="1122" height="173" alt="image" src="https://github.com/user-attachments/assets/33c6a797-11e8-4b5e-bd8f-56de1670db69" /> |
Denial of service. The call is synchronous native code, so it blocks the whole event loop, and nothing in JS (a timer, try/catch) can interrupt it. The pattern is usually fixed by the developer, so the only precondition is that an application splits attacker-controlled text with \b, \B or a multiline anchor. split(/\b/) is a common way to tokenize words, and the trigger is any long run of non-ASCII text, which is ordinary input for non-English users. A single request of about 1 MB costs minutes of CPU, and repeated requests keep a worker blocked. Bounded per request: unlike CVE-2026-68499 it terminates, and memory stays linear.
Implement ECMAScript SplitMatcher semantics. Keep p (end of the last split) and q (search start) separate. Use each match at its real position. When an empty match is at p, or any match is at the very end, advance q by one code point instead of splitting. q never moves backwards, so the loop is linear. Patch: q3_split_quadratic.patch (applies to 1.27.0; the 1.27.0 test suite passes). It also fixes the output: split(/\b/), ^/$ (m), (a)? and (a)| now match the built-in engine.
size_t p = 0, q = 0;
while (q < str.size && re2->regexp.Match(str, q, str.size, RE2::UNANCHORED, &groups[0], groups.size()))
{
size_t m = match.data() - str.data, e = m + match.size();
if (m >= str.size) break; // a match at the end never splits
if (e == p) { q = m + getUtf8CharSize(str.data[m], str.size - m); continue; } // empty match at p
pieces.push_back(re2::StringPiece(str.data + p, m - p));
if (pieces.size() >= limit) break;
pieces.insert(pieces.end(), groups.begin() + 1, groups.end());
if (pieces.size() >= limit) break;
p = q = e;
}
if (str.size && pieces.size() < limit) pieces.push_back(re2::StringPiece(str.data + p, str.size - p));Workaround: don't split untrusted text with \b, \B or multiline anchors. Use matchAll with a consuming pattern such as /\w+|\W+/g instead.
Source: NVD
Free Vulnerability Assessment
Evaluate your cloud security practices across 9 security domains to benchmark your risk level and identify gaps in your defenses.
Get a personalized demo
"Best User Experience I have ever seen, provides full visibility to cloud workloads."
"Wiz provides a single pane of glass to see what is going on in our cloud environments."
"We know that if Wiz identifies something as critical, it actually is."