75 lines
3.3 KiB
TypeScript
75 lines
3.3 KiB
TypeScript
import { describe, it } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { extractClientIp, getClientIpFromRequest } from "@/lib/ipUtils";
|
|
|
|
/**
|
|
* Regression tests for IP detection — ported from decolua/9router#1893.
|
|
*
|
|
* When OmniRoute runs behind a local reverse proxy (nginx etc.) the TCP peer
|
|
* is loopback (127.0.0.1 / ::1) and forwarding headers (X-Forwarded-For,
|
|
* X-Real-IP, CF-Connecting-IP) carry the real client IP. When the request
|
|
* arrives directly from the public internet, the TCP peer IS the client and
|
|
* forwarding headers are spoofable — keying brute-force buckets by them lets
|
|
* one attacker either lock everyone else out or evade the lockout entirely.
|
|
*/
|
|
|
|
function makeReq(headers: Record<string, string>, remoteAddress?: string) {
|
|
return {
|
|
headers: new Headers(headers),
|
|
socket: remoteAddress ? { remoteAddress } : undefined,
|
|
};
|
|
}
|
|
|
|
describe("ipUtils — loopback-gated forwarding headers", () => {
|
|
it("trusts X-Forwarded-For when TCP peer is 127.0.0.1 loopback", () => {
|
|
const req = makeReq({ "x-forwarded-for": "203.0.113.10" }, "127.0.0.1");
|
|
assert.equal(getClientIpFromRequest(req), "203.0.113.10");
|
|
});
|
|
|
|
it("trusts X-Forwarded-For when TCP peer is ::1 loopback", () => {
|
|
const req = makeReq({ "x-forwarded-for": "203.0.113.11" }, "::1");
|
|
assert.equal(getClientIpFromRequest(req), "203.0.113.11");
|
|
});
|
|
|
|
it("trusts X-Real-IP when TCP peer is loopback", () => {
|
|
const req = makeReq({ "x-real-ip": "203.0.113.12" }, "127.0.0.1");
|
|
assert.equal(getClientIpFromRequest(req), "203.0.113.12");
|
|
});
|
|
|
|
it("trusts CF-Connecting-IP when TCP peer is loopback", () => {
|
|
const req = makeReq({ "cf-connecting-ip": "203.0.113.13" }, "127.0.0.1");
|
|
assert.equal(getClientIpFromRequest(req), "203.0.113.13");
|
|
});
|
|
|
|
it("ignores spoofed X-Forwarded-For when TCP peer is a public address", () => {
|
|
// Direct public client trying to spoof another IP — must be ignored so
|
|
// the brute-force guard keys by the unspoofable TCP peer.
|
|
const req = makeReq({ "x-forwarded-for": "203.0.113.99" }, "198.51.100.5");
|
|
assert.equal(getClientIpFromRequest(req), "198.51.100.5");
|
|
});
|
|
|
|
it("ignores spoofed CF-Connecting-IP when TCP peer is a public address", () => {
|
|
const req = makeReq({ "cf-connecting-ip": "203.0.113.88" }, "198.51.100.7");
|
|
assert.equal(getClientIpFromRequest(req), "198.51.100.7");
|
|
});
|
|
|
|
it("falls back to forwarding headers when no socket peer is known", () => {
|
|
// Edge runtime / fetch path where req.socket is absent — preserve prior
|
|
// behavior, otherwise we'd lose all IPs in that path.
|
|
const req = makeReq({ "x-forwarded-for": "203.0.113.20" });
|
|
assert.equal(getClientIpFromRequest(req), "203.0.113.20");
|
|
});
|
|
|
|
it("returns loopback peer when no forwarding headers are present", () => {
|
|
const req = makeReq({}, "127.0.0.1");
|
|
assert.equal(getClientIpFromRequest(req), "127.0.0.1");
|
|
});
|
|
|
|
it("extractClientIp (lower-level) keeps prior contract", () => {
|
|
// Lower-level helper does NOT know the peer — preserve prior behavior.
|
|
assert.equal(extractClientIp("203.0.113.1, 10.0.0.1", "127.0.0.1"), "203.0.113.1");
|
|
assert.equal(extractClientIp(null, "198.51.100.1"), "198.51.100.1");
|
|
assert.equal(extractClientIp("unknown, 203.0.113.2", "10.0.0.1"), "203.0.113.2");
|
|
});
|
|
});
|