fix(api-response): strict IP validation via node:net.isIP

The looksLikeIp helper matched any input the hex/dot character class
accepted — `:::`, `1.2`, `gg:hh::1` all passed. The cf-connecting-ip
flow trusts the header under the env flag and the trusted-proxy XFF
chain forwards the rightmost entry straight to the rate-limiter +
audit log, so a malformed value landed downstream unchanged. Defer to
Node's built-in `isIP` which returns 4 / 6 for valid v4 / v6 and 0
for anything else. Add tests covering the structurally invalid v4 /
v6 / hex shapes plus a positive case for well-formed IPv6.
This commit is contained in:
Marc-André Bombeck committed 2026-05-17 17:23:02 +02:00
1 parent bc2bc65335
commit 14de24c15d
2 files changed
+46 -1

No files matched your search

+34
View File
@@ -101,6 +101,40 @@ describe("getClientIp trusted-proxy semantics (V3 audit)", () => {
expect(ip).toBe("5.6.7.8");
});
// v1.4.38 — looksLikeIp now defers to node:net's `isIP` instead of
// a hex/dot character-class regex. Structurally invalid candidates
// (`:::`, `1.2`, `gg:hh::1`) used to slip through the regex and
// pollute the rate-limit / audit log with non-IP strings; they are
// now rejected at the helper.
it("rejects structurally invalid IPv4 candidates (1.2)", () => {
const ip = getClientIp(
makeRequest({ "x-forwarded-for": "1.2, 5.6.7.8" }),
);
expect(ip).toBe("5.6.7.8");
});
it("rejects structurally invalid IPv6 candidates (:::)", () => {
const ip = getClientIp(
makeRequest({ "x-forwarded-for": ":::, 5.6.7.8" }),
);
expect(ip).toBe("5.6.7.8");
});
it("rejects hex-but-not-IPv6 chains", () => {
const ip = getClientIp(
makeRequest({ "x-forwarded-for": "gg:hh::1, 5.6.7.8" }),
);
expect(ip).toBe("5.6.7.8");
});
it("accepts well-formed IPv6 addresses", () => {
process.env.TRUST_PROXY_HOPS = "1";
const ip = getClientIp(
makeRequest({ "x-forwarded-for": "2001:db8::1" }),
);
expect(ip).toBe("2001:db8::1");
});
it("falls back to x-real-ip if XFF missing", () => {
const ip = getClientIp(makeRequest({ "x-real-ip": "5.6.7.8" }));
expect(ip).toBe("5.6.7.8");
+12 -1
View File
@@ -1,3 +1,4 @@
import { isIP } from "node:net";
import { NextResponse } from "next/server";
export function apiSuccess<T>(data: T, status = 200) {
@@ -86,8 +87,18 @@ export async function safeJson<T = unknown>(
*
* Returns the resolved IP or null when no trusted source is available.
*/
/**
* v1.4.38 — strict IP validation via Node's built-in `net.isIP`. The
* earlier regex `/^[0-9a-fA-F.:]+$/` matched any structurally-broken
* input the character set accepted (`:::`, `1.2`, `1.2.3`, `gg:hh::`).
* The cf-connecting-ip flow trusts the header under the env flag and
* the trusted-proxy XFF chain forwards the rightmost entry straight
* to the rate-limiter + audit log; a malformed value would land
* downstream unchanged. `isIP` returns 4 / 6 for valid v4 / v6 and 0
* for anything else — invert to a boolean for the helper's surface.
*/
function looksLikeIp(s: string): boolean {
return /^[0-9a-fA-F.:]+$/.test(s) && s.length >= 3 && s.length <= 45;
return isIP(s) !== 0;
}
/**