Skip to content

Commit ce7668b

Browse files
committed
fix: fix DDOS vulnerability when parsing headers
1 parent 9ba33ed commit ce7668b

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

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)