chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
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");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user