Update dependency re2 to v1.26.1 [SECURITY] - #137
Open
renovate[bot] wants to merge 1 commit into
Open
Conversation
renovate
Bot
force-pushed
the
renovate/npm-re2-vulnerability
branch
from
August 8, 2026 09:03
84b053c to
0ab7325
Compare
renovate
Bot
force-pushed
the
renovate/npm-re2-vulnerability
branch
from
August 26, 2026 14:12
0ab7325 to
ee0a549
Compare
renovate
Bot
force-pushed
the
renovate/npm-re2-vulnerability
branch
from
September 2, 2026 22:53
ee0a549 to
8441ca3
Compare
renovate
Bot
force-pushed
the
renovate/npm-re2-vulnerability
branch
from
September 7, 2026 23:56
8441ca3 to
8d7b218
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR contains the following updates:
1.17.1→1.26.1re2: Global
String.prototype.matchwith an empty-matchable pattern never advances → infinite loop with unbounded native memory growth (DoS)CVE-2026-68499 / GHSA-6hxr-mr5r-9836
More information
Details
Summary
String.prototype.matchwith a globalRE2collects 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 bytry/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
When
match.size() == 0,byteIndexis unchanged, so the next iteration matches the same empty position again;groupsgrows without bound. The other iteration paths already guard this:lib/split.cc:50-55advances bygetUtf8CharSizeon an empty match, andexecadvanceslastIndex. Only this globalMatchloop is missing the guard.Proof of concept
Compare with the built-in engine, which terminates:
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 beSIGKILLed externally.Impact
Denial of service. Reachable remotely and without authentication wherever an application runs a global
RE2throughString.prototype.matchand 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.Resolution
Fixed in re2 1.25.2.
The global match loop in
lib/match.ccnow advances the cursor by one Unicodecode 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 guardalready present in
split.Remediation: upgrade to
re2@1.25.2or later.Workaround (if you cannot upgrade): do not run a global
RE2throughString.prototype.matchwhen the pattern is attacker-influenced or can matchthe empty string. Iterate with
matchAll/exec, or use the non-global form;both already advanced the cursor correctly.
Severity
CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:HReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
re2: Out-of-bounds heap read in
exec/test/matchvia attacker-influencedlastIndexon a non-ASCII subject → uncatchable process crash (DoS)CVE-2026-67550 / GHSA-ff84-5f28-78qj
More information
Details
Summary
re2validates the user-settablelastIndexagainst 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 alastIndexbetween 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/catchcannot 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);setIndexthen validates the (UTF-16)lastIndexagainst that byte length and walks the buffer by character count:getUtf16PositionByCounterreadsdata[from]and advances by the UTF-8 char size with no check offromagainst the buffer size:lastIndexis 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
prepareArgumentfor anyglobal/stickyregex, reached byexec,test,String.prototype.match,replace, andsplit.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::Newataddon.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 cleannpm install re2@1.25.1(latest): stock prebuilt → SIGSEGV; ASAN build → the heap-buffer-overflow read above.Impact
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).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. ThislastIndexout-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:
(thread
sizethrough the two call sites inStrVal::setIndex).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.
lastIndexis now validated against the subject's UTF-16 length instead of itsUTF-8 byte length (
lib/addon.cc), so an out-of-rangelastIndexis rejectedbefore the buffer is walked. As defense in depth, the code-unit walk
(
getUtf16PositionByCounterinlib/wrapped_re2.h) is now bounded by thebuffer size and can no longer read past the end.
Remediation: upgrade to
re2@1.25.2or later.Workaround (if you cannot upgrade): do not assign
lastIndexfrom untrustedinput, or clamp it to the subject's string length (
str.length) before callingexec/test/match/replace/spliton a non-ASCII subject.Severity
CVSS:3.1/AV:L/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:HReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
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
CVE-2026-71430 / GHSA-8hcv-x26h-mcgp
More information
Details
Description
WrappedRE2::Replacebuilds the replacement result and hands it to V8 with.ToLocalChecked()without checking for the emptyMaybeLocalthat V8 returns when the string/buffer exceeds its maximum length:lib/replace.cc(v1.24.1):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'sString::kMaxLength(~536,870,888 chars on 64-bit).Nan::New(result)then returns an emptyMaybeLocal, and the unchecked.ToLocalChecked()callsv8::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/catchcannot 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
Observed (Node v24, clean
npm i re2→ re2@1.24.1): native branch printsRangeError: Invalid string length; the re2 branch aborts withFATAL ERROR: v8::ToLocalChecked Empty MaybeLocal, stack topWrappedRE2::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 nativeabort(), it cannot be contained bytry/catchor 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
MaybeLocalbeforeToLocalCheckedon both return paths (and the intermediate group-string builds), and throw a catchableRangeErrorto match the built-in engine:(Apply equivalently to the
Nan::CopyBuffer(...)buffer path at L553 and to the per-groupNan::New(data, size).ToLocalChecked()sites used by the replacer-function path.)Resolution
Resolved in
re21.25.1.WrappedRE2::Replacenow checks the returnedMaybeLocalon every result path and throws a catchableRangeError: Invalid string length(matching the built-in engine) instead of aborting the process with an uncatchableSIGABRT. No API changes --- upgrade tore2>=1.25.1via a plainnpm upgradeto receive the fix.Severity
CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:HReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
node-re2: Out-of-bounds heap read in
replace/splitvia aBufferending in a truncated multi-byte UTF-8 character → adjacent heap memory disclosed to JavaScriptCVE-2026-71498 / GHSA-j4r3-hg7j-8chg
More information
Details
Summary
re2infers a character's byte length from its UTF-8 lead byte alone, with no bound on thebytes actually remaining in the input.
Bufferarguments reach the native layer verbatim —only strings are re-encoded into well-formed UTF-8 — so a
Bufferwhose last byte is amulti-byte lead promises continuation bytes that are not there, and the result builders read
up to 3 bytes past the end of the buffer. In
replace()andsplit()those bytes are copiedinto the returned
Buffer, disclosing adjacent heap memory to JavaScript. The trigger isdeterministic and requires no special heap grooming.
Only
Bufferinput is affected. String input was never at risk: re-encoding guarantees everymulti-byte sequence is complete.
Root cause
getUtf8CharSizemaps a lead byte to a length of 1–4 and never sees the input size:Callers then read that many bytes. In the zero-width branch of
replace(), the guard provesonly that at least one byte remains:
offset < sizepermitsoffset == size - 1, so a lead byte of0xF0makesappendreaddata[size],data[size + 1]anddata[size + 2].Seven read sites shared the defect:
lib/replace.cc(zero-width branch)lib/replace.cc(callback replacer)lib/replace.cc(replacement scan)lib/split.cclib/pattern.cctranslateRegExp(x2)lib/pattern.ccescapeRegExpThree further callers were not vulnerable, because they use the result only to advance an
index and never dereference past the end:
getUtf16PositionByCounterinlib/wrapped_re2.h(clamps its return to the buffer size),
lib/match.cc(the value feedsRE2::Match, whichrejects
startpos > endpos), and thegetMaxSubmatchscan inlib/replace.cc(an overshootjust ends the loop).
Proof of concept
Each call returns more bytes than were supplied; the trailing bytes are heap contents and vary
between runs.
0xC2(2-byte lead) and0xE2(3-byte lead) over-read 1 and 2 bytes respectively;0xF0over-reads 3.
For the pattern path the over-read occurs in
translateRegExp/escapeRegExp, which runbefore RE2 validates the pattern, but RE2 then rejects the malformed input, so the bytes are
discarded rather than returned:
Impact
Information disclosure (
replace,split). Up to 3 bytes of heap memory adjacent to theinput buffer are returned to JavaScript per call. The read is repeatable, so an attacker who
controls
Bufferinput and observes output can sample heap memory incrementally. What landsthere depends on allocator layout and is not directly steerable, but it may include fragments
of other buffers.
Out-of-bounds read (pattern compilation). No disclosure path, since the malformed pattern
is rejected — but the read is still undefined behavior and can fault if the buffer ends on a
page boundary.
Applications that pass only strings, or only well-formed UTF-8 buffers, are unaffected. The
exposure matters most where
re2is used as intended: running patterns or subjects derivedfrom untrusted input.
Suggested fix
Clamp the inferred character size to the bytes that actually remain, at every site whose result
indexes the buffer:
This is O(1) and changes no algorithm's complexity. A truncated tail then round-trips as the
bytes it really holds, which preserves the documented contract that
Bufferinput is passedthrough verbatim. Rejecting malformed UTF-8 in
Bufferinput would also close the hole, butis a breaking API change.
Resolution
Fixed in
re2@1.26.1.All seven read sites now clamp the character size to the remaining input, so a
Bufferendingin a truncated multi-byte character round-trips as its own bytes instead of reading past the
end. Regression tests cover the subject, replacement and pattern positions for 2-, 3- and
4-byte leads, including partially truncated sequences.
Remediation: upgrade to
re2@1.26.1or later.Workaround (if you cannot upgrade): pass strings rather than
Buffers, or validate thatBufferinput is well-formed UTF-8 before callingreplace,split, or theRE2constructor — for example
Buffer.compare(Buffer.from(buf.toString('utf8')), buf) === 0.Reported by @OvOhao in #272.
Severity
CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:LReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
Release Notes
uhop/node-re2 (re2)
v1.26.1Compare Source
v1.26.0Compare Source
v1.25.2Compare Source
v1.25.1Compare Source
v1.25.0Compare Source
v1.24.1Compare Source
v1.24.0Compare Source
v1.23.3Compare Source
v1.23.2Compare Source
v1.23.1Compare Source
v1.23.0Compare Source
v1.22.3Compare Source
v1.22.2Compare Source
v1.22.1Compare Source
v1.22.0Compare Source
v1.21.5Compare Source
v1.21.4Compare Source
v1.21.3Compare Source
v1.21.2Compare Source
v1.21.1Compare Source
v1.21.0Compare Source
v1.20.12Compare Source
v1.20.11Compare Source
v1.20.10Compare Source
v1.20.9Compare Source
v1.20.8Compare Source
v1.20.7Compare Source
v1.20.5Compare Source
v1.20.4Compare Source
v1.20.3Compare Source
v1.20.2Compare Source
v1.20.1Compare Source
v1.20.0Compare Source
v1.19.2Compare Source
v1.19.1Compare Source
v1.19.0Compare Source
v1.18.3Compare Source
v1.18.2Compare Source
v1.18.1Compare Source
v1.18.0Compare Source
v1.17.8Compare Source
v1.17.7Compare Source
v1.17.6Compare Source
v1.17.4Compare Source
v1.17.3Compare Source
v1.17.2Compare Source
Configuration
📅 Schedule: (UTC)
🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.
♻ Rebasing: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.
🔕 Ignore: Close this PR and you won't be reminded about this update again.
This PR was generated by Mend Renovate. View the repository job log.