Skip to content

Commit b60e603

Browse files
kitsonkcursoragent
andauthored
fix: address ReDoS vulnerability in headers (#700)
Co-authored-by: Cursor Agent <[email protected]>
1 parent babb9c5 commit b60e603

File tree

2 files changed

+98
-5
lines changed

2 files changed

+98
-5
lines changed

request.test.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,3 +288,82 @@ Deno.test({
288288
);
289289
},
290290
});
291+
292+
Deno.test({
293+
name: "request.x-forwarded-for - splits, trims, and orders correctly",
294+
fn() {
295+
const request = new Request(
296+
createMockNativeRequest("https://example.com/index.html", {
297+
headers: {
298+
"x-forwarded-host": "example.com",
299+
"x-forwarded-proto": "http",
300+
"x-forwarded-for": " 10.10.10.10 , 192.168.1.1 , [::1] ",
301+
},
302+
}),
303+
{ proxy: true, secure: true },
304+
);
305+
assertEquals(request.ips, ["10.10.10.10", "192.168.1.1", "[::1]"]);
306+
assertEquals(request.ip, "10.10.10.10");
307+
},
308+
});
309+
310+
Deno.test({
311+
name: "request.x-forwarded-for - caps entries and is performant",
312+
fn() {
313+
const manyIps = Array.from({ length: 1000 }, (_, i) => `10.0.0.${i}`).join(
314+
", ",
315+
);
316+
const request = new Request(
317+
createMockNativeRequest("https://example.com/index.html", {
318+
headers: {
319+
"x-forwarded-host": "example.com",
320+
"x-forwarded-proto": "http",
321+
// also prepend some whitespace noise to mimic worst-case patterns
322+
"x-forwarded-for": ` \t ${manyIps} \t `,
323+
},
324+
}),
325+
{ proxy: true, secure: true },
326+
);
327+
performance.mark("start-xff");
328+
const ips = request.ips;
329+
const measure = performance.measure("xff", { start: "start-xff" });
330+
// Hard upper bound; the operation should be very fast
331+
assert(measure.duration < 20);
332+
// Ensure we cap the number of parsed IPs (implementation caps at 100)
333+
assertEquals(ips.length, 100);
334+
assertEquals(ips[0], "10.0.0.0");
335+
},
336+
});
337+
338+
Deno.test({
339+
name: "request.x-forwarded-proto - normalizes and allowlists http/https",
340+
fn() {
341+
const request = new Request(
342+
createMockNativeRequest("http://example.com/index.html", {
343+
headers: {
344+
"x-forwarded-host": "example.com",
345+
"x-forwarded-proto": " HTTPS , http ",
346+
},
347+
}),
348+
{ proxy: true },
349+
);
350+
assertEquals(request.url.protocol, "https:");
351+
},
352+
});
353+
354+
Deno.test({
355+
name: "request.x-forwarded-proto - invalid values fall back to http",
356+
fn() {
357+
const request = new Request(
358+
createMockNativeRequest("http://example.com/index.html", {
359+
headers: {
360+
"x-forwarded-host": "example.com",
361+
// first token invalid, second valid, we only honor the first
362+
"x-forwarded-proto": "javascript, https",
363+
},
364+
}),
365+
{ proxy: true },
366+
);
367+
assertEquals(request.url.protocol, "http:");
368+
},
369+
});

request.ts

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,15 @@ export class Request {
8383
* `X-Forwarded-For`. When `false` an empty array is returned. */
8484
get ips(): string[] {
8585
return this.#proxy
86-
? (this.#serverRequest.headers.get("x-forwarded-for") ??
87-
this.#getRemoteAddr()).split(/\s*,\s*/)
86+
? (() => {
87+
const raw = this.#serverRequest.headers.get("x-forwarded-for") ??
88+
this.#getRemoteAddr();
89+
const bounded = raw.length > 4096 ? raw.slice(0, 4096) : raw;
90+
return bounded
91+
.split(",", 100)
92+
.map((part) => part.trim())
93+
.filter((part) => part.length > 0);
94+
})()
8895
: [];
8996
}
9097

@@ -138,9 +145,16 @@ export class Request {
138145
let proto: string;
139146
let host: string;
140147
if (this.#proxy) {
141-
proto = serverRequest
142-
.headers.get("x-forwarded-proto")?.split(/\s*,\s*/, 1)[0] ??
143-
"http";
148+
const xForwardedProto = serverRequest.headers.get(
149+
"x-forwarded-proto",
150+
);
151+
let maybeProto = xForwardedProto
152+
? xForwardedProto.split(",", 1)[0].trim().toLowerCase()
153+
: undefined;
154+
if (maybeProto !== "http" && maybeProto !== "https") {
155+
maybeProto = undefined;
156+
}
157+
proto = maybeProto ?? "http";
144158
host = serverRequest.headers.get("x-forwarded-host") ??
145159
this.#url?.hostname ??
146160
serverRequest.headers.get("host") ??

0 commit comments

Comments
 (0)