Skip to content

Commit 399257c

Browse files
committed
fix(cookie): prevent unbounded chunked cookie count
1 parent 459a1c6 commit 399257c

File tree

2 files changed

+46
-1
lines changed

2 files changed

+46
-1
lines changed

src/utils/cookie.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,11 +241,19 @@ function _getDistinctCookieKey(name: string, options: Partial<SetCookie>) {
241241
return [name, options.domain || "", options.path || "/"].join(";");
242242
}
243243

244+
// Maximum number of chunks allowed for chunked cookies.
245+
// 100 chunks × ~4KB = ~400KB, far beyond any practical cookie size.
246+
const MAX_CHUNKED_COOKIE_COUNT = 100;
247+
244248
function getChunkedCookieCount(cookie: string | undefined): number {
245249
if (!cookie?.startsWith(CHUNKED_COOKIE)) {
246250
return Number.NaN;
247251
}
248-
return Number.parseInt(cookie.slice(CHUNKED_COOKIE.length));
252+
const count = Number.parseInt(cookie.slice(CHUNKED_COOKIE.length));
253+
if (Number.isNaN(count) || count < 0 || count > MAX_CHUNKED_COOKIE_COUNT) {
254+
return Number.NaN;
255+
}
256+
return count;
249257
}
250258

251259
function chunkCookieName(name: string, chunkNumber: number): string {

test/cookies.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
setCookie,
55
getChunkedCookie,
66
setChunkedCookie,
7+
deleteChunkedCookie,
78
} from "../src/utils/cookie.ts";
89
import { describeMatrix } from "./_setup.ts";
910

@@ -153,6 +154,42 @@ describeMatrix("cookies", (t, { it, expect, describe }) => {
153154
});
154155
});
155156

157+
describe("chunked cookie DoS protection", () => {
158+
it("setChunkedCookie ignores excessively large chunk count from request cookie", async () => {
159+
t.app.get("/", (event) => {
160+
// This should NOT loop 999999 times
161+
setChunkedCookie(event, "session", "newvalue", {
162+
chunkMaxLength: 5,
163+
});
164+
return "200";
165+
});
166+
const result = await t.fetch("/", {
167+
headers: {
168+
Cookie: "session=__chunked__999999",
169+
},
170+
});
171+
expect(await result.text()).toBe("200");
172+
// Should only have the new cookie set, no massive cleanup
173+
expect(result.headers.getSetCookie().length).toBeLessThan(10);
174+
});
175+
176+
it("deleteChunkedCookie ignores excessively large chunk count from request cookie", async () => {
177+
t.app.get("/", (event) => {
178+
// This should NOT loop 999999 times
179+
deleteChunkedCookie(event, "session");
180+
return "200";
181+
});
182+
const result = await t.fetch("/", {
183+
headers: {
184+
Cookie: "session=__chunked__999999",
185+
},
186+
});
187+
expect(await result.text()).toBe("200");
188+
// Should only delete the main cookie, not 999999 chunks
189+
expect(result.headers.getSetCookie().length).toBeLessThan(10);
190+
});
191+
});
192+
156193
describe("setChunkedCookie", () => {
157194
it("can set-cookie with setChunkedCookie", async () => {
158195
t.app.get("/", (event) => {

0 commit comments

Comments
 (0)