Mar 23, 2026·7 min read·4 visits
h3 fails to validate the chunk count in chunked cookies. An attacker can supply an artificially high chunk count, forcing the server into an expensive O(n²) loop during session cleanup, completely blocking the Node.js event loop and causing a Denial of Service.
A logic-based Denial of Service (DoS) vulnerability exists in the h3 minimal HTTP framework, commonly used in Nuxt and Nitro applications. The vulnerability is caused by an unbounded loop in the chunked cookie parsing logic, allowing remote unauthenticated attackers to exhaust server resources and block the Node.js event loop.
The h3 framework is a minimal HTTP framework designed for high performance and portability. It serves as the core server engine for Nitro and, by extension, the Nuxt framework ecosystem. One of the features provided by h3 is utility functions for managing HTTP cookies. Because web browsers generally enforce a size limit of approximately 4 kilobytes per cookie, applications storing large session objects must implement a mechanism to split these objects across multiple smaller cookies.
The h3 framework implements this using "chunked cookies". When a cookie exceeds the standard size limit, it is segmented into multiple chunks. The framework tracks these chunks by setting a primary tracking cookie containing a sentinel value indicating the total number of segments. This sentinel value typically follows a predictable string pattern, such as __chunked__5, where the integer suffix represents the segment count.
A critical vulnerability exists in the logic responsible for parsing this sentinel value. The function extracting the integer suffix does not validate the upper bounds of the supplied value. Consequently, an attacker can construct a malicious HTTP request containing a forged cookie header with an arbitrarily large segment count.
When the framework attempts to update or invalidate this session, it initiates a cleanup routine that iterates based on the user-supplied segment count. Due to the single-threaded nature of the Node.js event loop, forcing the framework into an excessively large synchronous loop consumes all available CPU time on the main thread. This prevents the server from processing concurrent HTTP requests, resulting in a severe application-layer Denial of Service (DoS).
The root cause of this vulnerability lies in the implementation of the getChunkedCookieCount function located in src/utils/cookie.ts. This function is responsible for analyzing an incoming HTTP Cookie header and extracting the numerical count of expected chunks.
The function relies on Number.parseInt() to extract the integer from a sliced string payload. Crucially, the implementation assumes the parsed integer will reflect a legitimate, application-generated chunk count. The code fails to implement any boundary checks, type enforcement beyond basic parsing, or sanity validations on the resulting integer.
The resulting unbounded integer is subsequently utilized as a loop condition parameter in session cleanup functions, such as setChunkedCookie and deleteChunkedCookie. When a session lifecycle event triggers the deletion of old session chunks, the framework initiates a for loop iterating from zero up to the user-supplied chunk count.
Inside this cleanup loop, the framework invokes the deleteCookie utility function. The deleteCookie function internally delegates to setCookie to append a new Set-Cookie header with a past expiration date. To prevent duplicate headers, setCookie scans all existing Set-Cookie headers attached to the current HTTP response object. As the loop iterates, the array of existing headers grows linearly. The nested header scanning operation against a linearly growing array results in an algorithmic complexity of O(n²), where n is the unvalidated chunk count controlled by the attacker. This quadratic scaling drastically exacerbates the CPU exhaustion.
The vulnerable implementation of getChunkedCookieCount demonstrates a classic failure to perform boundary validation on external input. The original code unconditionally extracts and returns the parsed integer.
function getChunkedCookieCount(cookie: string | undefined): number {
if (!cookie?.startsWith(CHUNKED_COOKIE)) {
return Number.NaN;
}
return Number.parseInt(cookie.slice(CHUNKED_COOKIE.length));
}If an attacker supplies the value __chunked__1000000, the slice operation isolates the string 1000000. The Number.parseInt() function converts this string to the integer 1000000. This integer is returned to the calling function and used directly in the subsequent for loop condition.
The patch introduced in commit 399257cb406fbeda313d88c1e288a15124fc82af addresses this flaw by defining a hard upper limit and implementing strict validation logic.
const MAX_CHUNKED_COOKIE_COUNT = 100;
function getChunkedCookieCount(cookie: string | undefined): number {
if (!cookie?.startsWith(CHUNKED_COOKIE)) {
return Number.NaN;
}
const count = Number.parseInt(cookie.slice(CHUNKED_COOKIE.length));
if (Number.isNaN(count) || count < 0 || count > MAX_CHUNKED_COOKIE_COUNT) {
return Number.NaN;
}
return count;
}The introduction of MAX_CHUNKED_COOKIE_COUNT = 100 establishes a rational boundary. A limit of 100 chunks allows for approximately 400 kilobytes of session data, which far exceeds practical browser limitations while remaining computationally trivial for the server to process. The patch validates that the parsed value is a valid number, is not negative, and does not exceed the predefined maximum.
Exploiting this vulnerability requires no specialized tools, advanced network positioning, or authentication. An attacker only requires network reachability to an exposed application endpoint that interacts with the h3 chunked cookie utilities.
The attacker crafts a standard HTTP GET or POST request. The critical component of the payload is the Cookie header, which must be structured to match the framework's expected sentinel pattern. The attacker defines a session cookie using the __chunked__ prefix followed by an excessively large integer.
GET /api/session-endpoint HTTP/1.1
Host: vulnerable-app.com
Cookie: session=__chunked__1000000Upon receiving this request, the target application invokes the vulnerable parsing logic. The Node.js JavaScript engine (V8) begins executing the synchronous cleanup loop. Because Node.js utilizes a single-threaded event loop for JavaScript execution, the V8 engine cannot yield control back to the libuv thread pool while executing a synchronous for loop.
The process remains blocked until the loop completes. During this time, the application cannot accept new connections, process existing asynchronous callbacks, or respond to health checks. Requests queue at the operating system or reverse proxy level until timeouts occur, typically resulting in 502 Bad Gateway or 504 Gateway Timeout errors.
The impact of this vulnerability is an application-level Denial of Service. While network-level DoS attacks require substantial bandwidth and infrastructure, this algorithmic complexity vulnerability allows a highly asymmetric attack. A single HTTP request, consuming mere bytes of bandwidth, forces the server to expend significant CPU cycles.
The CVSS v3.1 base score is calculated as 7.5 (High). The attack vector is Network (AV:N), the attack complexity is Low (AC:L), and no privileges or user interaction are required (PR:N, UI:N). The impact on Availability is High (A:H), while Confidentiality and Integrity remain uncompromised (C:N, I:N).
The consequences are particularly severe in containerized or orchestrated environments. If the application is unable to respond to readiness or liveness probes from a system like Kubernetes due to event loop starvation, the orchestrator may forcefully terminate and restart the pod. An attacker sending periodic payload requests can induce a continuous crash loop, resulting in persistent downtime.
Furthermore, the attack scales efficiently against horizontally scaled infrastructure. By distributing the malicious payload across the available IP addresses or load balancer endpoints, an attacker can systematically exhaust the CPU resources of the entire server fleet.
The primary and most effective remediation strategy is to upgrade the h3 dependency to a patched release. Administrators must ensure that their deployment utilizes v2.0.1-rc.18 or a subsequent stable version. In ecosystem frameworks like Nuxt or Nitro, running standard package manager update commands (npm update or yarn upgrade) will typically resolve the transitive dependency.
For environments where immediate patching is not feasible, security engineers can deploy mitigations at the edge network layer. A Web Application Firewall (WAF) rule can be authored to inspect incoming HTTP Cookie headers. The rule should utilize a regular expression to identify anomalous chunk counts. A regex pattern such as __chunked__[0-9]{3,} will detect any request attempting to declare 100 or more chunks, allowing the edge device to drop the request before it reaches the vulnerable application.
Additionally, operations teams should review reverse proxy configurations. Most modern proxies, such as Nginx or HAProxy, implement default limits on HTTP header sizes. While header size limits do not prevent this specific logic flaw (as the payload itself is very short), ensuring aggressive enforcement of request boundaries provides defense-in-depth against similar application-layer exhaustion attacks.
Developers maintaining custom cookie parsing logic should adopt the validation patterns demonstrated in the official patch. All user-supplied boundary markers, lengths, and loop conditions must be subjected to strict upper-bound limits and mathematical validation before being utilized in operational logic.
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H| Product | Affected Versions | Fixed Version |
|---|---|---|
h3 Unjs | < 2.0.1-rc.18 | 2.0.1-rc.18 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-606, CWE-400 |
| Attack Vector | Network |
| CVSS Base Score | 7.5 |
| Impact | High (Denial of Service) |
| Exploit Status | Proof of Concept |
| KEV Status | Not Listed |
The product does not check or incorrectly checks input that is used as a loop condition, potentially leading to a denial of service or other consequences.