Описание
re2: Out-of-bounds heap read in exec/test/match via attacker-influenced lastIndex on a non-ASCII subject → uncatchable process crash (DoS)
Summary
re2 validates the user-settable lastIndex against the subject's UTF-8 byte length but then uses it as a UTF-16 code-unit count to walk the subject buffer, with no bounds check. For any non-ASCII subject, the byte length is larger than the true character count, so a lastIndex between those two values passes validation while pointing past the end of the buffer. The subsequent walk reads out of bounds. With a large subject the read marches into unmapped memory and the process dies with SIGABRT/SIGSEGV — an uncatchable crash (try/catch cannot stop it), i.e. a denial of service for any worker/process that runs the match. In some cases the out-of-bounds bytes are copied into the returned value (a bounded, best-effort heap information leak).
Root cause
The subject wrapper stores the UTF-8 byte length in StrVal::length:
lib/addon.cc:200auto argLength = utf8Length(s, isolate);— UTF-8 byte countlib/addon.cc:209lastStringValue.reset(buffer, argSize, argLength, startFrom, false, isAscii);
setIndex then validates the (UTF-16) lastIndex against that byte length and walks the buffer by character count:
getUtf16PositionByCounter reads data[from] and advances by the UTF-8 char size with no check of from against the buffer size:
lastIndex is user-settable to any positive integer (capped only at >= 0, no upper bound):
For an ASCII subject the byte length equals the UTF-16 length, so the guard is correct — this only triggers on non-ASCII subjects. The out-of-bounds read happens inside prepareArgument for any global/sticky regex, reached by exec, test, String.prototype.match, replace, and split.
Proof of concept
Minimal (AddressSanitizer, deterministic OOB read):
Built with -fsanitize=address, this aborts with:
The overflowed region is the subject buffer allocated by node::Buffer::New at addon.cc:205.
Real-world impact on the shipped prebuilt binary (no ASAN) — uncatchable crash:
try { ... } catch (e) {} around the call does not prevent termination — it is a native fault, not a JS exception. Validated on a clean npm install re2@1.25.1 (latest): stock prebuilt → SIGSEGV; ASAN build → the heap-buffer-overflow read above.
Impact
- Denial of service (primary): an uncatchable native crash that terminates the Node process/worker. Reachable remotely and without authentication wherever an application (a) uses a
globalorstickyRE2, (b) applies it to a non-ASCII subject, and (c) setslastIndexfrom attacker-influenced data (e.g. resuming a scan/pagination at a client-supplied offset). - Information disclosure (secondary, best-effort): the out-of-bounds
byteIndexcan cause adjacent heap bytes to be copied into the returned value (e.g. the leading segment of areplaceresult). This is bounded and unreliable — the subject buffer iscalloc-allocated (zero-filled) and the over-read distance depends on interpreting out-of-bounds bytes as UTF-8 sizes — so it is noted for completeness, not as a dependable primitive.
This is distinct from GHSA-8hcv-x26h-mcgp (the global replace() output-amplification abort), which was fixed in 1.25.1. This lastIndex out-of-bounds read is a separate defect and remains present in 1.25.1.
Suggested fix
Two independent hardenings; either closes the crash, both is safest:
- Bound the walk so it can never read past the buffer:
(thread size through the two call sites in StrVal::setIndex).
- Validate
lastIndexagainst the true UTF-16 length, not the UTF-8 byte length — e.g. stores->Length()(UTF-16 units) as the value compared inisValidIndex = newIndex <= <utf16Length>, so an out-of-rangelastIndextakes the existing!isValidIndexearly-return path.
Resolution
Fixed in re2 1.25.2.
lastIndex is now validated against the subject's UTF-16 length instead of its
UTF-8 byte length (lib/addon.cc), so an out-of-range lastIndex is rejected
before the buffer is walked. As defense in depth, the code-unit walk
(getUtf16PositionByCounter in lib/wrapped_re2.h) is now bounded by the
buffer size and can no longer read past the end.
Remediation: upgrade to re2@1.25.2 or later.
Workaround (if you cannot upgrade): do not assign lastIndex from untrusted
input, or clamp it to the subject's string length (str.length) before calling
exec/test/match/replace/split on a non-ASCII subject.
Пакеты
re2
<= 1.25.1
1.25.2
Связанные уязвимости
(re2 provides Node.js bindings for Google's RE2 regular expression engi ...)
re2 provides Node.js bindings for Google's RE2 regular expression engine. Prior to 1.25.2, re2 validates lastIndex against the UTF-8 byte length of a subject but uses it as a UTF-16 code-unit offset in exec, test, match, replace, and split, allowing an attacker-influenced lastIndex on a non-ASCII subject to trigger an out-of-bounds heap read and an uncatchable process crash, with limited heap information disclosure in some cases. This issue is fixed in 1.25.2.
re2 provides Node.js bindings for Google's RE2 regular expression engi ...