Skip to content

fix(deps): update dependency re2 to v1.26.1 [security] - #17855

Open
renovate[bot] wants to merge 1 commit into
developfrom
renovate/npm-re2-vulnerability
Open

fix(deps): update dependency re2 to v1.26.1 [security]#17855
renovate[bot] wants to merge 1 commit into
developfrom
renovate/npm-re2-vulnerability

Conversation

@renovate

@renovate renovate Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

This PR contains the following updates:

Package Change Age Confidence
re2 1.26.01.26.1 age confidence

node-re2: Out-of-bounds heap read in replace/split via a Buffer ending in a truncated multi-byte UTF-8 character → adjacent heap memory disclosed to JavaScript

CVE-2026-71498 / GHSA-j4r3-hg7j-8chg

More information

Details

Summary

re2 infers a character's byte length from its UTF-8 lead byte alone, with no bound on the
bytes actually remaining in the input. Buffer arguments reach the native layer verbatim —
only strings are re-encoded into well-formed UTF-8 — so a Buffer whose last byte is a
multi-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() and split() those bytes are copied
into the returned Buffer, disclosing adjacent heap memory to JavaScript. The trigger is
deterministic and requires no special heap grooming.

Only Buffer input is affected. String input was never at risk: re-encoding guarantees every
multi-byte sequence is complete.

Root cause

getUtf8CharSize maps a lead byte to a length of 1–4 and never sees the input size:

// lib/wrapped_re2.h
inline size_t getUtf8CharSize(char ch)
{
      return ((0xE5000000 >> ((ch >> 3) & 0x1E)) & 3) + 1;
}

Callers then read that many bytes. In the zero-width branch of replace(), the guard proves
only that at least one byte remains:

// lib/replace.cc
else if ((size_t)offset < size)
{
      auto sym_size = getUtf8CharSize(data[offset]);   // may claim up to 4 bytes
      result.append(data + offset, sym_size);          // reads data[offset .. offset + 3]
      byteIndex = offset + sym_size;
}

offset < size permits offset == size - 1, so a lead byte of 0xF0 makes append read
data[size], data[size + 1] and data[size + 2].

Seven read sites shared the defect:

Site Argument Disclosed to JS
lib/replace.cc (zero-width branch) subject yes
lib/replace.cc (callback replacer) subject yes
lib/replace.cc (replacement scan) replacement yes
lib/split.cc subject yes
lib/pattern.cc translateRegExp (x2) pattern no
lib/pattern.cc escapeRegExp pattern no

Three further callers were not vulnerable, because they use the result only to advance an
index and never dereference past the end: getUtf16PositionByCounter in lib/wrapped_re2.h
(clamps its return to the buffer size), lib/match.cc (the value feeds RE2::Match, which
rejects startpos > endpos), and the getMaxSubmatch scan in lib/replace.cc (an overshoot
just ends the loop).

Proof of concept

Each call returns more bytes than were supplied; the trailing bytes are heap contents and vary
between runs.

const RE2 = require('re2');
const hex = buf => [...buf].map(b => b.toString(16).padStart(2, '0')).join(' ');

// subject: 2 bytes in, 5 bytes out
console.log(hex(new RE2('', 'g').replace(Buffer.from([0x41, 0xf0]), '')));
// 41 f0 61 7b eb   <- last 3 bytes are adjacent heap memory

// replacement argument
console.log(hex(new RE2('A', 'g').replace(Buffer.from('A'), Buffer.from([0x42, 0xf0]))));
// 42 f0 41 26 d6

// split
console.log(new RE2('', 'g').split(Buffer.from([0x41, 0xf0])).map(hex));
// [ '41', 'f0 e2 e4 df' ]

0xC2 (2-byte lead) and 0xE2 (3-byte lead) over-read 1 and 2 bytes respectively; 0xF0
over-reads 3.

For the pattern path the over-read occurs in translateRegExp / escapeRegExp, which run
before RE2 validates the pattern, but RE2 then rejects the malformed input, so the bytes are
discarded rather than returned:

new RE2(Buffer.from([0xf0]));   // SyntaxError: invalid UTF-8 — read already happened
Impact

Information disclosure (replace, split). Up to 3 bytes of heap memory adjacent to the
input buffer are returned to JavaScript per call. The read is repeatable, so an attacker who
controls Buffer input and observes output can sample heap memory incrementally. What lands
there 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 re2 is used as intended: running patterns or subjects derived
from untrusted input.

Suggested fix

Clamp the inferred character size to the bytes that actually remain, at every site whose result
indexes the buffer:

inline size_t getUtf8CharSize(char ch, size_t remaining)
{
      size_t size = getUtf8CharSize(ch);
      return size < remaining ? size : remaining;
}

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 Buffer input is passed
through verbatim. Rejecting malformed UTF-8 in Buffer input would also close the hole, but
is 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 Buffer ending
in 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.1 or later.

Workaround (if you cannot upgrade): pass strings rather than Buffers, or validate that
Buffer input is well-formed UTF-8 before calling replace, split, or the RE2
constructor — for example Buffer.compare(Buffer.from(buf.toString('utf8')), buf) === 0.

Reported by @​OvOhao in #​272.

Severity

  • CVSS Score: 5.1 / 10 (Medium)
  • Vector String: CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:L

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


node-re2: Out-of-bounds heap read in replace/split via a Buffer ending in a truncated multi-byte UTF-8 character → adjacent heap memory disclosed to JavaScript

CVE-2026-71498 / GHSA-j4r3-hg7j-8chg

More information

Details

Summary

re2 infers a character's byte length from its UTF-8 lead byte alone, with no bound on the
bytes actually remaining in the input. Buffer arguments reach the native layer verbatim —
only strings are re-encoded into well-formed UTF-8 — so a Buffer whose last byte is a
multi-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() and split() those bytes are copied
into the returned Buffer, disclosing adjacent heap memory to JavaScript. The trigger is
deterministic and requires no special heap grooming.

Only Buffer input is affected. String input was never at risk: re-encoding guarantees every
multi-byte sequence is complete.

Root cause

getUtf8CharSize maps a lead byte to a length of 1–4 and never sees the input size:

// lib/wrapped_re2.h
inline size_t getUtf8CharSize(char ch)
{
      return ((0xE5000000 >> ((ch >> 3) & 0x1E)) & 3) + 1;
}

Callers then read that many bytes. In the zero-width branch of replace(), the guard proves
only that at least one byte remains:

// lib/replace.cc
else if ((size_t)offset < size)
{
      auto sym_size = getUtf8CharSize(data[offset]);   // may claim up to 4 bytes
      result.append(data + offset, sym_size);          // reads data[offset .. offset + 3]
      byteIndex = offset + sym_size;
}

offset < size permits offset == size - 1, so a lead byte of 0xF0 makes append read
data[size], data[size + 1] and data[size + 2].

Seven read sites shared the defect:

Site Argument Disclosed to JS
lib/replace.cc (zero-width branch) subject yes
lib/replace.cc (callback replacer) subject yes
lib/replace.cc (replacement scan) replacement yes
lib/split.cc subject yes
lib/pattern.cc translateRegExp (x2) pattern no
lib/pattern.cc escapeRegExp pattern no

Three further callers were not vulnerable, because they use the result only to advance an
index and never dereference past the end: getUtf16PositionByCounter in lib/wrapped_re2.h
(clamps its return to the buffer size), lib/match.cc (the value feeds RE2::Match, which
rejects startpos > endpos), and the getMaxSubmatch scan in lib/replace.cc (an overshoot
just ends the loop).

Proof of concept

Each call returns more bytes than were supplied; the trailing bytes are heap contents and vary
between runs.

const RE2 = require('re2');
const hex = buf => [...buf].map(b => b.toString(16).padStart(2, '0')).join(' ');

// subject: 2 bytes in, 5 bytes out
console.log(hex(new RE2('', 'g').replace(Buffer.from([0x41, 0xf0]), '')));
// 41 f0 61 7b eb   <- last 3 bytes are adjacent heap memory

// replacement argument
console.log(hex(new RE2('A', 'g').replace(Buffer.from('A'), Buffer.from([0x42, 0xf0]))));
// 42 f0 41 26 d6

// split
console.log(new RE2('', 'g').split(Buffer.from([0x41, 0xf0])).map(hex));
// [ '41', 'f0 e2 e4 df' ]

0xC2 (2-byte lead) and 0xE2 (3-byte lead) over-read 1 and 2 bytes respectively; 0xF0
over-reads 3.

For the pattern path the over-read occurs in translateRegExp / escapeRegExp, which run
before RE2 validates the pattern, but RE2 then rejects the malformed input, so the bytes are
discarded rather than returned:

new RE2(Buffer.from([0xf0]));   // SyntaxError: invalid UTF-8 — read already happened
Impact

Information disclosure (replace, split). Up to 3 bytes of heap memory adjacent to the
input buffer are returned to JavaScript per call. The read is repeatable, so an attacker who
controls Buffer input and observes output can sample heap memory incrementally. What lands
there 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 re2 is used as intended: running patterns or subjects derived
from untrusted input.

Suggested fix

Clamp the inferred character size to the bytes that actually remain, at every site whose result
indexes the buffer:

inline size_t getUtf8CharSize(char ch, size_t remaining)
{
      size_t size = getUtf8CharSize(ch);
      return size < remaining ? size : remaining;
}

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 Buffer input is passed
through verbatim. Rejecting malformed UTF-8 in Buffer input would also close the hole, but
is 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 Buffer ending
in 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.1 or later.

Workaround (if you cannot upgrade): pass strings rather than Buffers, or validate that
Buffer input is well-formed UTF-8 before calling replace, split, or the RE2
constructor — for example Buffer.compare(Buffer.from(buf.toString('utf8')), buf) === 0.

Reported by @​OvOhao in #​272.

Severity

  • CVSS Score: 5.1 / 10 (Medium)
  • Vector String: CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:L

References

This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).


Release Notes

uhop/node-re2 (re2)

v1.26.1

Compare Source


Configuration

📅 Schedule: (in timezone Asia/Tokyo)

  • Branch creation
    • At any time (no schedule defined)
  • Automerge
    • At any time (no schedule defined)

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


  • If you want to rebase/retry this PR, check this box

This PR was generated by Mend Renovate. View the repository job log.

@renovate renovate Bot added the dependencies Pull requests that update a dependency file label Aug 7, 2026
@github-actions github-actions Bot added the packages/backend Server side specific issue/PR label Aug 7, 2026
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

このPRによるapi.jsonの差分
差分はありません。
Get diff files from Workflow Page

@codecov

codecov Bot commented Aug 7, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 26.13%. Comparing base (b812ddb) to head (dee467a).
⚠️ Report is 1 commits behind head on develop.

Additional details and impacted files
@@             Coverage Diff              @@
##           develop   #17855       +/-   ##
============================================
+ Coverage    13.99%   26.13%   +12.14%     
============================================
  Files          248     1175      +927     
  Lines        12041    40114    +28073     
  Branches      4036    11118     +7082     
============================================
+ Hits          1685    10485     +8800     
- Misses        8118    23779    +15661     
- Partials      2238     5850     +3612     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

🖥 Frontend Diagnostics Report

(No significant changes)

View details

Requests by resource type
Type Requests Encoded bytes
Base Head Δ Base Head Δ
Document 2 2 0 33 KB 33 KB 0 B
Script 148 148 0 2.1 MB 2.1 MB 0 B
Stylesheet 58 58 0 288 KB 288 KB 0 B
Fetch 20 20 0 46 KB 46 KB 0 B
Image 10 10 0 254 KB 254 KB 0 B
Font 2 2 0 118 KB 118 KB 0 B
Other 9 9 0 421 KB 421 KB $\color{orange}{\text{+10 B}}$
V8 heap snapshot statistics
Metric @ Base @ Head Δ MAD
$\color{gray}{\rule{8pt}{8pt}}$ Total 11 MB
± 6.1 KB
11 MB
± 4.6 KB
$\text{-8 KB}$
$\text{-0.1\%}$
7.6 KB
$\color{orange}{\rule{8pt}{8pt}}$ Code 2.8 MB 2.8 MB $\text{-4.1 KB}$ 3.6 KB
$\color{red}{\rule{8pt}{8pt}}$ Strings 1.2 MB 1.2 MB $\text{+16 B}$ 124 B
$\color{cyan}{\rule{8pt}{8pt}}$ JS arrays 120 KB 120 KB 0 B 0 B
$\color{green}{\rule{8pt}{8pt}}$ Typed arrays 0 B 0 B 0 B 0 B
$\color{yellow}{\rule{8pt}{8pt}}$ System objects 0 B 0 B 0 B 0 B
$\color{violet}{\rule{8pt}{8pt}}$ Other JS objs 2.1 MB 2.1 MB 0 B 32 B
$\color{pink}{\rule{8pt}{8pt}}$ Other non-JS objs 5 MB 5 MB $\text{-4.5 KB}$ 6.7 KB

Download representative heap snapshot: base / head

📦 Bundle Stats

Chunk size diff (0 updated, 0 added, 0 removed)
Chunk Base Head Δ Δ (%)
(total) 5.8 MB 5.8 MB 0 B 0%
(other generated chunks) 2 MB 2 MB 0 B 0%
Startup chunk size (0 updated, 0 added, 0 removed)
Chunk Base Head Δ Δ (%)
(total) 1 MB 1 MB 0 B 0%
(other generated chunks) 754 KB 754 KB 0 B 0%
(other) 280 KB 280 KB 0 B 0%

Startup chunks are the Vite entry for src/_boot_.ts and its static imports.

Bundles Modules Entries Imports Size
Static Dynamic Rendered Gzip Brotli
Base 468 2,861 21 10,662 332 10 MB 2.8 MB 2.4 MB
Head 468 2,861 21 10,662 332 10 MB 2.8 MB 2.4 MB
Δ 0 0 0 0 0 0 B $\text{-2 B}$ $\text{-14 B}$
Δ (%) 0% 0% 0% 0% 0% 0% $\text{-0\%}$ $\text{-0\%}$

Open treemap HTML

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

⚙️ Backend Diagnostics Report

Memory: After GC

(No significant changes)

V8 Heap Snapshot Statistics

Metric @ Base @ Head Δ MAD
$\color{gray}{\rule{8pt}{8pt}}$ Total 86 MB
± 704 B
86 MB
± 2.8 KB
$\text{+888 B}$
$\text{+0\%}$
2.9 KB
$\color{orange}{\rule{8pt}{8pt}}$ Code 25 MB 25 MB 0 B 0 B
$\color{red}{\rule{8pt}{8pt}}$ Strings 30 MB 30 MB $\text{+48 B}$ 321 B
$\color{cyan}{\rule{8pt}{8pt}}$ JS arrays 4.2 MB 4.2 MB $\text{+80 B}$ 204 B
$\color{green}{\rule{8pt}{8pt}}$ Typed arrays 566 KB 566 KB 0 B 0 B
$\color{yellow}{\rule{8pt}{8pt}}$ System objects 2.2 MB 2.2 MB $\text{+1.1 KB}$ 223 B
$\color{violet}{\rule{8pt}{8pt}}$ Other JS objs 24 MB 24 MB $\text{+88 B}$ 79 B
$\color{pink}{\rule{8pt}{8pt}}$ Other non-JS objs 39 KB 39 KB 0 B 0 B

Download representative heap snapshot: base / head

@renovate
renovate Bot force-pushed the renovate/npm-re2-vulnerability branch from 6ec7ef8 to a32c42c Compare August 12, 2026 02:58
@renovate
renovate Bot force-pushed the renovate/npm-re2-vulnerability branch from a32c42c to dee467a Compare August 14, 2026 21:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file packages/backend Server side specific issue/PR

Projects

Development

Successfully merging this pull request may close these issues.

0 participants