Skip to content

Commit 1e9a156

Browse files
committed
util/get-client-ip: also normalize IPv6 with port numbers
1 parent f6bcdf0 commit 1e9a156

File tree

2 files changed

+117
-22
lines changed

2 files changed

+117
-22
lines changed

src/packages/util/get-client-ip-address.test.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,15 @@ describe("getClientIpAddress()", () => {
188188
expect(result).toBe("192.0.2.60");
189189
});
190190

191+
it("should handle IPv4 addresses with ports in X-Forwarded-For", () => {
192+
const req = createRequest({
193+
"x-forwarded-for": "192.168.1.1:8080",
194+
});
195+
196+
const result = getClientIpAddress(req);
197+
expect(result).toBe("192.168.1.1");
198+
});
199+
191200
it("should handle multiple parameters in Forwarded header", () => {
192201
const req = createRequest({
193202
forwarded: "for=192.0.2.60;proto=http;by=203.0.113.43",
@@ -223,6 +232,33 @@ describe("getClientIpAddress()", () => {
223232
const result = getClientIpAddress(req);
224233
expect(result).toBeUndefined();
225234
});
235+
236+
it("should handle Forwarded header with spaces around commas", () => {
237+
const req = createRequest({
238+
forwarded: "for=192.0.2.60 , for=203.0.113.43",
239+
});
240+
241+
const result = getClientIpAddress(req);
242+
expect(result).toBe("192.0.2.60");
243+
});
244+
245+
it("should handle Forwarded header with spaces around semicolons", () => {
246+
const req = createRequest({
247+
forwarded: "for=192.0.2.60 ; proto=http ; by=203.0.113.43",
248+
});
249+
250+
const result = getClientIpAddress(req);
251+
expect(result).toBe("192.0.2.60");
252+
});
253+
254+
it("should handle Forwarded header with mixed spacing", () => {
255+
const req = createRequest({
256+
forwarded: " for=192.0.2.60 ; proto=http ; by=203.0.113.43 ",
257+
});
258+
259+
const result = getClientIpAddress(req);
260+
expect(result).toBe("192.0.2.60");
261+
});
226262
});
227263

228264
describe("IPv6 Support", () => {
@@ -252,6 +288,24 @@ describe("getClientIpAddress()", () => {
252288
const result = getClientIpAddress(req);
253289
expect(result).toBe("::1");
254290
});
291+
292+
it("should handle IPv6 addresses with ports", () => {
293+
const req = createRequest({
294+
"x-forwarded-for": "[2001:db8::1]:8080",
295+
});
296+
297+
const result = getClientIpAddress(req);
298+
expect(result).toBe("2001:db8::1");
299+
});
300+
301+
it("should handle compressed IPv6 addresses with ports", () => {
302+
const req = createRequest({
303+
"x-forwarded-for": "[::1]:9000",
304+
});
305+
306+
const result = getClientIpAddress(req);
307+
expect(result).toBe("::1");
308+
});
255309
});
256310

257311
describe("Edge Cases", () => {
@@ -288,5 +342,23 @@ describe("getClientIpAddress()", () => {
288342
const result = getClientIpAddress(req);
289343
expect(result).toBe("192.168.1.1");
290344
});
345+
346+
it("should handle IP addresses with whitespace", () => {
347+
const req = createRequest({
348+
"x-forwarded-for": " 192.168.1.1 ",
349+
});
350+
351+
const result = getClientIpAddress(req);
352+
expect(result).toBe("192.168.1.1");
353+
});
354+
355+
it("should handle IPv6 addresses with whitespace and ports", () => {
356+
const req = createRequest({
357+
"x-forwarded-for": " [2001:db8::1]:8080 ",
358+
});
359+
360+
const result = getClientIpAddress(req);
361+
expect(result).toBe("2001:db8::1");
362+
});
291363
});
292364
});

src/packages/util/get-client-ip-address.ts

Lines changed: 45 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,9 @@ export function getClientIpAddress(req: {
2323
// Handle comma-separated values (like X-Forwarded-For)
2424
const ips = headerValue.split(",").map((ip) => ip.trim());
2525
for (const ip of ips) {
26-
if (isIP(ip)) {
27-
return ip;
26+
const processedIp = normalizeIPAddress(ip);
27+
if (isIP(processedIp)) {
28+
return processedIp;
2829
}
2930
}
3031
}
@@ -40,36 +41,24 @@ export function getClientIpAddress(req: {
4041
// https://github.com/pbojinov/request-ip/pull/71
4142
const forwardedHeader = getHeaderValue(req.headers, "forwarded");
4243
if (forwardedHeader) {
43-
// Split by comma for multiple forwarded entries
44-
const forwardedEntries = forwardedHeader.split(",");
44+
// Split by comma for multiple forwarded entries, trimming each entry
45+
const forwardedEntries = forwardedHeader.split(",").map(entry => entry.trim());
4546

4647
for (const entry of forwardedEntries) {
47-
// Split by semicolon for parameters
48-
const params = entry.split(";");
48+
// Split by semicolon for parameters, trimming each parameter
49+
const params = entry.split(";").map(param => param.trim());
4950

5051
for (const param of params) {
51-
const trimmed = param.trim();
52-
if (trimmed.toLowerCase().startsWith("for=")) {
53-
let ipVal = trimmed.substring(4).trim();
52+
if (param.toLowerCase().startsWith("for=")) {
53+
let ipVal = param.substring(4).trim();
5454

5555
// Remove quotes if present
5656
if (ipVal.startsWith('"') && ipVal.endsWith('"')) {
5757
ipVal = ipVal.slice(1, -1);
5858
}
5959

60-
// Handle IPv6 brackets
61-
if (ipVal.startsWith("[") && ipVal.endsWith("]")) {
62-
ipVal = ipVal.slice(1, -1);
63-
}
64-
65-
// Handle port stripping for IPv4 addresses
66-
if (ipVal.includes(":")) {
67-
const parts = ipVal.split(":");
68-
// Only strip port if it looks like IPv4:port (not IPv6)
69-
if (parts.length === 2 && isIP(parts[0])) {
70-
ipVal = parts[0];
71-
}
72-
}
60+
// Normalize IP address (remove brackets and ports)
61+
ipVal = normalizeIPAddress(ipVal);
7362

7463
if (isIP(ipVal)) {
7564
return ipVal;
@@ -82,6 +71,40 @@ export function getClientIpAddress(req: {
8271
return undefined;
8372
}
8473

74+
// Helper function to normalize IP address by removing brackets and ports
75+
function normalizeIPAddress(ip: string): string {
76+
let processedIp = ip.trim();
77+
78+
// Remove IPv6 brackets if present (do this first!)
79+
const bracketStart = processedIp.startsWith("[");
80+
const closingBracketIndex = processedIp.indexOf("]");
81+
const hasPortAfterBracket = closingBracketIndex > 0 && processedIp[closingBracketIndex + 1] === ":";
82+
if (bracketStart && hasPortAfterBracket) {
83+
// Extract IPv6 part and port: [2001:db8::1]:8080 -> 2001:db8::1:8080
84+
processedIp = processedIp.substring(1, closingBracketIndex) + processedIp.substring(closingBracketIndex + 1);
85+
} else if (processedIp.startsWith("[") && processedIp.endsWith("]")) {
86+
// Simple bracket removal: [2001:db8::1] -> 2001:db8::1
87+
processedIp = processedIp.slice(1, -1);
88+
}
89+
90+
// Strip port if present (handles both IPv4:port and IPv6:port)
91+
if (processedIp.includes(":")) {
92+
const lastColonIndex = processedIp.lastIndexOf(":");
93+
if (lastColonIndex > 0) {
94+
const potentialPort = processedIp.substring(lastColonIndex + 1);
95+
// If the part after the last colon looks like a port number
96+
if (/^\d+$/.test(potentialPort)) {
97+
const potentialIP = processedIp.substring(0, lastColonIndex);
98+
if (isIP(potentialIP)) {
99+
processedIp = potentialIP;
100+
}
101+
}
102+
}
103+
}
104+
105+
return processedIp;
106+
}
107+
85108
// Helper function to get header value case-insensitively
86109
function getHeaderValue(
87110
headers: Record<string, string | string[] | undefined>,

0 commit comments

Comments
 (0)