mirror of
https://github.com/jakejarvis/sofa.git
synced 2026-10-16 01:05:42 -04:00
Resolve the client IP from the TCP peer plus trusted-proxy X-Forwarded-For hops (TRUSTED_PROXIES) and hand it to Better Auth in an internal header, so spoofed forwarding headers can no longer rotate sign-in rate-limit buckets. The Expo authorization proxy now only redirects to the OIDC provider's authorization endpoint. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
110 lines
3.6 KiB
TypeScript
110 lines
3.6 KiB
TypeScript
import { getIP } from "@better-auth/core/utils/ip";
|
|
import { describe, expect, test } from "vitest";
|
|
|
|
import { CLIENT_IP_HEADER } from "@sofa/auth/config";
|
|
|
|
import {
|
|
buildTrustedProxyList,
|
|
DEFAULT_TRUSTED_PROXIES,
|
|
normalizeAddress,
|
|
parseTrustedProxies,
|
|
resolveClientIp,
|
|
} from "../src/client-ip";
|
|
|
|
const defaults = buildTrustedProxyList(DEFAULT_TRUSTED_PROXIES).list;
|
|
const none = buildTrustedProxyList([]).list;
|
|
|
|
describe("resolveClientIp", () => {
|
|
test("direct internet client spoofing X-Forwarded-For is keyed on the peer", () => {
|
|
expect(resolveClientIp("6.6.6.6", "203.0.113.9", defaults)).toBe("203.0.113.9");
|
|
});
|
|
|
|
test("direct internet client with no header", () => {
|
|
expect(resolveClientIp(null, "203.0.113.9", defaults)).toBe("203.0.113.9");
|
|
});
|
|
|
|
test("LAN client direct, no header (all-trusted fallback)", () => {
|
|
expect(resolveClientIp(null, "192.168.1.5", defaults)).toBe("192.168.1.5");
|
|
});
|
|
|
|
test("dual-stack mapped peer is normalized", () => {
|
|
expect(resolveClientIp(null, "::ffff:192.168.1.5", defaults)).toBe("192.168.1.5");
|
|
});
|
|
|
|
test("local proxy, internet client", () => {
|
|
expect(resolveClientIp("198.51.100.7", "172.18.0.5", defaults)).toBe("198.51.100.7");
|
|
});
|
|
|
|
test("local proxy, LAN client", () => {
|
|
expect(resolveClientIp("192.168.1.5", "127.0.0.1", defaults)).toBe("192.168.1.5");
|
|
});
|
|
|
|
test("spoof through a local proxy", () => {
|
|
expect(resolveClientIp("6.6.6.6, 198.51.100.7", "172.18.0.5", defaults)).toBe("198.51.100.7");
|
|
});
|
|
|
|
test("malformed hop left of a LAN client", () => {
|
|
expect(resolveClientIp("garbage, 192.168.1.5", "127.0.0.1", defaults)).toBe("192.168.1.5");
|
|
});
|
|
|
|
test("malformed hop right after the peer", () => {
|
|
expect(resolveClientIp("garbage", "127.0.0.1", defaults)).toBe("127.0.0.1");
|
|
});
|
|
|
|
test("trust nothing", () => {
|
|
expect(resolveClientIp("6.6.6.6", "127.0.0.1", none)).toBe("127.0.0.1");
|
|
});
|
|
|
|
test("IPv6 client through a local proxy", () => {
|
|
expect(resolveClientIp("2001:db8::1", "::1", defaults)).toBe("2001:db8::1");
|
|
});
|
|
|
|
test("unknown peer", () => {
|
|
expect(resolveClientIp("6.6.6.6", undefined, defaults)).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("TRUSTED_PROXIES parsing", () => {
|
|
test("parseTrustedProxies", () => {
|
|
expect(parseTrustedProxies(undefined)).toBe(DEFAULT_TRUSTED_PROXIES);
|
|
expect(parseTrustedProxies(" ")).toBe(DEFAULT_TRUSTED_PROXIES);
|
|
expect(parseTrustedProxies("None")).toEqual([]);
|
|
expect(parseTrustedProxies(" 1.2.3.4 , 10.0.0.0/24 ")).toEqual(["1.2.3.4", "10.0.0.0/24"]);
|
|
});
|
|
|
|
test("buildTrustedProxyList reports invalid entries", () => {
|
|
expect(
|
|
buildTrustedProxyList([
|
|
"1.2.3.4",
|
|
"10.0.0.0/24",
|
|
"nope",
|
|
"10.0.0.0/33",
|
|
"::1/129",
|
|
"1.2.3.4/8/1",
|
|
"10.0.0.0/x",
|
|
]).invalid,
|
|
).toEqual(["nope", "10.0.0.0/33", "::1/129", "1.2.3.4/8/1", "10.0.0.0/x"]);
|
|
expect(buildTrustedProxyList(DEFAULT_TRUSTED_PROXIES).invalid).toEqual([]);
|
|
});
|
|
|
|
test("normalizeAddress leaves plain IPv6 unchanged", () => {
|
|
expect(normalizeAddress("2001:db8::1")).toBe("2001:db8::1");
|
|
});
|
|
});
|
|
|
|
describe("Better Auth reads the resolved header", () => {
|
|
const options = { advanced: { ipAddress: { ipAddressHeaders: [CLIENT_IP_HEADER] } } };
|
|
|
|
test("uses the internal header and ignores a client-sent X-Forwarded-For", () => {
|
|
expect(getIP(new Headers({ [CLIENT_IP_HEADER]: "198.51.100.7" }), options)).toBe(
|
|
"198.51.100.7",
|
|
);
|
|
expect(
|
|
getIP(
|
|
new Headers({ [CLIENT_IP_HEADER]: "192.168.1.5", "x-forwarded-for": "6.6.6.6" }),
|
|
options,
|
|
),
|
|
).toBe("192.168.1.5");
|
|
});
|
|
});
|