Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions request.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,3 +288,82 @@ Deno.test({
);
},
});

Deno.test({
name: "request.x-forwarded-for - splits, trims, and orders correctly",
fn() {
const request = new Request(
createMockNativeRequest("https://example.com/index.html", {
headers: {
"x-forwarded-host": "example.com",
"x-forwarded-proto": "http",
"x-forwarded-for": " 10.10.10.10 , 192.168.1.1 , [::1] ",
},
}),
{ proxy: true, secure: true },
);
assertEquals(request.ips, ["10.10.10.10", "192.168.1.1", "[::1]"]);
assertEquals(request.ip, "10.10.10.10");
},
});

Deno.test({
name: "request.x-forwarded-for - caps entries and is performant",
fn() {
const manyIps = Array.from({ length: 1000 }, (_, i) => `10.0.0.${i}`).join(
", ",
);
const request = new Request(
createMockNativeRequest("https://example.com/index.html", {
headers: {
"x-forwarded-host": "example.com",
"x-forwarded-proto": "http",
// also prepend some whitespace noise to mimic worst-case patterns
"x-forwarded-for": ` \t ${manyIps} \t `,
},
}),
{ proxy: true, secure: true },
);
performance.mark("start-xff");
const ips = request.ips;
const measure = performance.measure("xff", { start: "start-xff" });
// Hard upper bound; the operation should be very fast
assert(measure.duration < 20);
// Ensure we cap the number of parsed IPs (implementation caps at 100)
assertEquals(ips.length, 100);
assertEquals(ips[0], "10.0.0.0");
},
});

Deno.test({
name: "request.x-forwarded-proto - normalizes and allowlists http/https",
fn() {
const request = new Request(
createMockNativeRequest("http://example.com/index.html", {
headers: {
"x-forwarded-host": "example.com",
"x-forwarded-proto": " HTTPS , http ",
},
}),
{ proxy: true },
);
assertEquals(request.url.protocol, "https:");
},
});

Deno.test({
name: "request.x-forwarded-proto - invalid values fall back to http",
fn() {
const request = new Request(
createMockNativeRequest("http://example.com/index.html", {
headers: {
"x-forwarded-host": "example.com",
// first token invalid, second valid, we only honor the first
"x-forwarded-proto": "javascript, https",
},
}),
{ proxy: true },
);
assertEquals(request.url.protocol, "http:");
},
});
24 changes: 19 additions & 5 deletions request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,15 @@ export class Request {
* `X-Forwarded-For`. When `false` an empty array is returned. */
get ips(): string[] {
return this.#proxy
? (this.#serverRequest.headers.get("x-forwarded-for") ??
this.#getRemoteAddr()).split(/\s*,\s*/)
? (() => {
const raw = this.#serverRequest.headers.get("x-forwarded-for") ??
this.#getRemoteAddr();
const bounded = raw.length > 4096 ? raw.slice(0, 4096) : raw;
return bounded
.split(",", 100)
.map((part) => part.trim())
.filter((part) => part.length > 0);
})()
: [];
}

Expand Down Expand Up @@ -138,9 +145,16 @@ export class Request {
let proto: string;
let host: string;
if (this.#proxy) {
proto = serverRequest
.headers.get("x-forwarded-proto")?.split(/\s*,\s*/, 1)[0] ??
"http";
const xForwardedProto = serverRequest.headers.get(
"x-forwarded-proto",
);
let maybeProto = xForwardedProto
? xForwardedProto.split(",", 1)[0].trim().toLowerCase()
: undefined;
if (maybeProto !== "http" && maybeProto !== "https") {
maybeProto = undefined;
}
proto = maybeProto ?? "http";
host = serverRequest.headers.get("x-forwarded-host") ??
this.#url?.hostname ??
serverRequest.headers.get("host") ??
Expand Down