GitHub Security Advisory: GHSA-q5pr-72pq-83v3
Package: h3 (NPM)
Severity: MODERATE (CVSS 5.3)
CWE: CWE-400: Uncontrolled Resource Consumption
Vulnerable versions: >= 2.0.0-beta.4, < 2.0.1-rc.18
Patched in: 2.0.1-rc.18

Summary: H3: Unbounded Chunked Cookie Count in Session Cleanup Loop may Lead to Denial of Service

Details:
## 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 <= previousChunkCount; i++) {
      deleteCookie(event, chunkCookieName(name, i), options);
      // Each deleteCookie → setCookie → scans ALL existing set-cookie headers
    }
  }
}
```

The same issue exists in `deleteChunkedCookie()` at `src/utils/cookie.ts:227-232`:

```typescript
const chunksCount = getChunkedCookieCount(mainCookie);
if (chunksCount >= 0) {
  for (let i = 0; i < chunksCount; i++) {
    deleteCookie(event, chunkCookieName(name, i + 1), serializeOptions);
  }
}
```

**The exploit chain through sessions:**

1. Attacker sends `Cookie: h3=__chunked__999999` to any session-using endpoint
2. `getSession()` (`src/utils/session.ts:83`) calls `getChunkedCookie(event, "h3")` (line 124)
3. `getChunkedCookie()` returns `undefined` — the early return at line 153 fires because no actual chunk cookies (e.g., `h3.1`) exist in the request
4. Since `sealedSession` is undefined, `session.id` remains empty (line 140), triggering `updateSession()` (line 143)
5. `updateSession()` calls `setChunkedCookie()` with the newly sealed session value (line 179)
6. Inside `setChunkedCookie()`, `getCookie(event, name)` re-reads the original request cookie `__chunked__999999` at line 182
7. `previousChunkCount` = 999999, `chunkCount` = 1 (new sealed session is small)
8. The cleanup loop runs 999,998 iterations, each calling `deleteCookie()` → `setCookie()`
9. Each `setCookie()` call reads ALL existing `set-cookie` response headers via `getSetCookie()` (line 91) and iterates through them for deduplicatio