mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
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:
1 parent
bc2bc65335
commit
14de24c15d
2 files changed
+46
-1
No files matched your search
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user