Files
Ekklesia/core/tests/identity-client-address.test.ts
T
ekklesiaandClaude Sonnet 5 cafd513019 fix: secure authentication request identity
Replace Better Auth's default trust of a caller-controlled X-Forwarded-For
with a platform-owned client-address resolution boundary: direct mode (the
safe default) trusts only the real socket peer, and an explicit trusted-proxy
mode requires a header name, a concrete trusted-proxy list, and only honors a
forwarded chain from an actual trusted peer. This closes a rate-limit bypass
where rotating a spoofed header let a caller choose a fresh bucket on every
request. Public sign-up is also disabled by default and requires an explicit
typed opt-in, using Better Auth's own disableSignUp gate so no email-existence
leak or second signup path is introduced.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-07-26 12:20:51 -05:00

351 lines
13 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
ClientAddressConfigError,
createClientAddressConfig,
createClientAddressResolver,
findInvalidTrustedProxies,
normalizeClientAddress,
} from "../src/identity/client-address.js";
const INTERNAL_HEADER = "x-ekklesia-client-address";
describe("createClientAddressConfig", () => {
it("defaults to direct mode when mode is entirely omitted", () => {
expect(createClientAddressConfig()).toEqual({ mode: "direct" });
expect(createClientAddressConfig({})).toEqual({ mode: "direct" });
expect(Object.isFrozen(createClientAddressConfig())).toBe(true);
});
it("rejects direct mode combined with a header or trusted proxies", () => {
expect(() => createClientAddressConfig({ header: "x-forwarded-for", mode: "direct" })).toThrow(
ClientAddressConfigError,
);
try {
createClientAddressConfig({ mode: "direct", trustedProxies: ["10.0.0.1"] });
} catch (error) {
expect(error).toMatchObject({ code: "CLIENT_ADDRESS_DIRECT_MODE_CONTRADICTION" });
}
});
it("rejects an invalid mode value", () => {
let captured: unknown;
try {
createClientAddressConfig({ mode: "proxy" });
} catch (error) {
captured = error;
}
expect(captured).toMatchObject({ code: "CLIENT_ADDRESS_MODE_INVALID" });
});
it("accepts a valid trusted-proxy configuration", () => {
const config = createClientAddressConfig({
header: "X-Forwarded-For",
mode: "trusted-proxy",
trustedProxies: ["10.0.0.5", "192.168.1.0/24"],
});
expect(config).toEqual({
header: "x-forwarded-for",
mode: "trusted-proxy",
trustedProxies: ["10.0.0.5", "192.168.1.0/24"],
});
expect(Object.isFrozen(config)).toBe(true);
if (config.mode === "trusted-proxy") {
expect(Object.isFrozen(config.trustedProxies)).toBe(true);
}
});
it("rejects trusted-proxy mode missing a header", () => {
let captured: unknown;
try {
createClientAddressConfig({ mode: "trusted-proxy", trustedProxies: ["10.0.0.1"] });
} catch (error) {
captured = error;
}
expect(captured).toMatchObject({ code: "CLIENT_ADDRESS_HEADER_MISSING" });
});
it("rejects an invalid header name", () => {
let captured: unknown;
try {
createClientAddressConfig({
header: "not a header!",
mode: "trusted-proxy",
trustedProxies: ["10.0.0.1"],
});
} catch (error) {
captured = error;
}
expect(captured).toMatchObject({ code: "CLIENT_ADDRESS_HEADER_INVALID" });
});
it("rejects the internal platform header as a configured source header", () => {
let captured: unknown;
try {
createClientAddressConfig(
{
header: INTERNAL_HEADER,
mode: "trusted-proxy",
trustedProxies: ["10.0.0.1"],
},
INTERNAL_HEADER,
);
} catch (error) {
captured = error;
}
expect(captured).toMatchObject({ code: "CLIENT_ADDRESS_HEADER_INVALID" });
});
it("rejects trusted-proxy mode with an empty trusted-proxy list", () => {
let captured: unknown;
try {
createClientAddressConfig({
header: "x-forwarded-for",
mode: "trusted-proxy",
trustedProxies: [],
});
} catch (error) {
captured = error;
}
expect(captured).toMatchObject({ code: "CLIENT_ADDRESS_TRUSTED_PROXIES_EMPTY" });
});
it("rejects a malformed trusted-proxy entry", () => {
for (const trustedProxies of [
["not-an-ip"],
["10.0.0.1/33"],
["10.0.0.1/-1"],
["10.0.0.1/1e3"],
[""],
]) {
let captured: unknown;
try {
createClientAddressConfig({
header: "x-forwarded-for",
mode: "trusted-proxy",
trustedProxies,
});
} catch (error) {
captured = error;
}
expect(captured).toMatchObject({ code: "CLIENT_ADDRESS_TRUSTED_PROXY_INVALID" });
}
});
it("never leaks the offending mode, header, or trusted-proxy value in the thrown error", () => {
const hostileHeader = "x-hostile-secret-header";
let captured: unknown;
try {
createClientAddressConfig({
header: hostileHeader,
mode: "trusted-proxy",
trustedProxies: ["definitely-not-an-ip"],
});
} catch (error) {
captured = error;
}
const publicText = `${String(captured)}\n${
captured instanceof Error ? (captured.stack ?? "") : ""
}`;
expect(publicText).not.toContain(hostileHeader);
expect(publicText).not.toContain("definitely-not-an-ip");
});
});
describe("findInvalidTrustedProxies", () => {
it("identifies exactly the malformed entries", () => {
expect(
findInvalidTrustedProxies(["10.0.0.1", "not-an-ip", "192.168.0.0/24", "10.0.0.1/99"]),
).toEqual(["not-an-ip", "10.0.0.1/99"]);
});
it("accepts IPv6 addresses and ranges", () => {
expect(findInvalidTrustedProxies(["::1", "fd00::/8", "2001:db8::1"])).toEqual([]);
});
});
describe("normalizeClientAddress", () => {
it("passes a plain IPv4 address through unchanged", () => {
expect(normalizeClientAddress("192.0.2.10")).toBe("192.0.2.10");
});
it("lowercases a plain IPv6 address", () => {
expect(normalizeClientAddress("2001:DB8::1")).toBe("2001:db8::1");
});
it("collapses an IPv4-mapped IPv6 address to its embedded IPv4 form", () => {
expect(normalizeClientAddress("::ffff:127.0.0.1")).toBe("127.0.0.1");
expect(normalizeClientAddress("::FFFF:203.0.113.5")).toBe("203.0.113.5");
});
it("rejects a zone id as ambiguous", () => {
expect(normalizeClientAddress("fe80::1%eth0")).toBeUndefined();
});
it("rejects malformed, empty, or non-IP input", () => {
for (const value of ["", " ", "not-an-ip", "999.999.999.999", "1.2.3"]) {
expect(normalizeClientAddress(value)).toBeUndefined();
}
});
it("trims surrounding whitespace", () => {
expect(normalizeClientAddress(" 192.0.2.10 ")).toBe("192.0.2.10");
});
});
describe("createClientAddressResolver — direct mode", () => {
it("resolves the actual immediate socket peer, ignoring any forwarded header", () => {
const resolver = createClientAddressResolver({ mode: "direct" });
const result = resolver.resolve({
headers: new Headers({ "x-forwarded-for": "203.0.113.99" }),
peerAddress: "192.0.2.1",
});
expect(result).toEqual({ address: "192.0.2.1", ok: true });
});
it("fails closed when no peer address is available", () => {
const resolver = createClientAddressResolver({ mode: "direct" });
const result = resolver.resolve({ headers: new Headers(), peerAddress: undefined });
expect(result).toEqual({ ok: false, reason: "PEER_ADDRESS_UNAVAILABLE" });
});
it("fails closed when the peer address is not a valid IP", () => {
const resolver = createClientAddressResolver({ mode: "direct" });
const result = resolver.resolve({ headers: new Headers(), peerAddress: "not-an-ip" });
expect(result).toEqual({ ok: false, reason: "PEER_ADDRESS_UNAVAILABLE" });
});
it("normalizes an IPv4-mapped IPv6 peer to its IPv4 form", () => {
const resolver = createClientAddressResolver({ mode: "direct" });
const result = resolver.resolve({ headers: new Headers(), peerAddress: "::ffff:198.51.100.7" });
expect(result).toEqual({ address: "198.51.100.7", ok: true });
});
it("gives distinct real peer addresses distinct resolved identities", () => {
const resolver = createClientAddressResolver({ mode: "direct" });
const first = resolver.resolve({ headers: new Headers(), peerAddress: "127.0.0.2" });
const second = resolver.resolve({ headers: new Headers(), peerAddress: "127.0.0.3" });
expect(first).toEqual({ address: "127.0.0.2", ok: true });
expect(second).toEqual({ address: "127.0.0.3", ok: true });
expect(first).not.toEqual(second);
});
});
describe("createClientAddressResolver — trusted-proxy mode", () => {
function proxyResolver(trustedProxies: readonly string[] = ["10.0.0.1", "10.0.0.2"]) {
return createClientAddressResolver({
header: "x-forwarded-for",
mode: "trusted-proxy",
trustedProxies,
});
}
it("ignores the forwarded header entirely when the immediate peer is not a trusted proxy", () => {
const resolver = proxyResolver();
const result = resolver.resolve({
headers: new Headers({ "x-forwarded-for": "203.0.113.99" }),
peerAddress: "198.51.100.1",
});
// Falls back to direct-mode behavior: the untrusted peer itself, never the header content.
expect(result).toEqual({ address: "198.51.100.1", ok: true });
});
it("honors a single-value forwarded header from a trusted proxy", () => {
const resolver = proxyResolver();
const result = resolver.resolve({
headers: new Headers({ "x-forwarded-for": "203.0.113.5" }),
peerAddress: "10.0.0.1",
});
expect(result).toEqual({ address: "203.0.113.5", ok: true });
});
it("walks a chained forwarded header right-to-left, skipping trusted-proxy hops", () => {
const resolver = proxyResolver();
const result = resolver.resolve({
headers: new Headers({ "x-forwarded-for": "203.0.113.5, 10.0.0.2" }),
peerAddress: "10.0.0.1",
});
expect(result).toEqual({ address: "203.0.113.5", ok: true });
});
it("walks past multiple trusted-proxy hops to find the genuine client", () => {
const resolver = proxyResolver(["10.0.0.1", "10.0.0.2", "10.0.0.3"]);
const result = resolver.resolve({
headers: new Headers({ "x-forwarded-for": "203.0.113.5, 10.0.0.3, 10.0.0.2" }),
peerAddress: "10.0.0.1",
});
expect(result).toEqual({ address: "203.0.113.5", ok: true });
});
it("fails closed when the trusted proxy sends no forwarded header at all", () => {
const resolver = proxyResolver();
const result = resolver.resolve({ headers: new Headers(), peerAddress: "10.0.0.1" });
expect(result).toEqual({ ok: false, reason: "FORWARDED_HEADER_MISSING" });
});
it("fails closed on an empty (but present) forwarded header value", () => {
const resolver = proxyResolver();
const result = resolver.resolve({
headers: new Headers({ "x-forwarded-for": "" }),
peerAddress: "10.0.0.1",
});
expect(result).toEqual({ ok: false, reason: "FORWARDED_HEADER_MALFORMED" });
});
it("fails closed on a malformed token anywhere in the chain", () => {
const resolver = proxyResolver();
const result = resolver.resolve({
headers: new Headers({ "x-forwarded-for": "203.0.113.5, not-an-ip" }),
peerAddress: "10.0.0.1",
});
expect(result).toEqual({ ok: false, reason: "FORWARDED_HEADER_MALFORMED" });
});
it("fails closed on an empty comma-separated segment (ambiguous multi-valued form)", () => {
const resolver = proxyResolver();
const result = resolver.resolve({
headers: new Headers({ "x-forwarded-for": "203.0.113.5,, 10.0.0.1" }),
peerAddress: "10.0.0.1",
});
expect(result).toEqual({ ok: false, reason: "FORWARDED_HEADER_MALFORMED" });
});
it("fails closed when the entire chain resolves to trusted-proxy hops with no genuine client left", () => {
const resolver = proxyResolver();
const result = resolver.resolve({
headers: new Headers({ "x-forwarded-for": "10.0.0.2, 10.0.0.1" }),
peerAddress: "10.0.0.1",
});
expect(result).toEqual({ ok: false, reason: "FORWARDED_CHAIN_EXHAUSTED" });
});
it("normalizes an IPv4-mapped IPv6 hop before matching it against a trusted IPv4 proxy", () => {
const resolver = proxyResolver(["10.0.0.1"]);
const result = resolver.resolve({
headers: new Headers({ "x-forwarded-for": "203.0.113.5, ::ffff:10.0.0.1" }),
peerAddress: "10.0.0.1",
});
expect(result).toEqual({ address: "203.0.113.5", ok: true });
});
it("matches a trusted-proxy CIDR range, not only an exact address", () => {
const resolver = proxyResolver(["192.168.1.0/24"]);
const result = resolver.resolve({
headers: new Headers({ "x-forwarded-for": "203.0.113.5" }),
peerAddress: "192.168.1.42",
});
expect(result).toEqual({ address: "203.0.113.5", ok: true });
});
it("never lets a spoofed header from an untrusted peer choose a client identity that collides with a trusted flow", () => {
const resolver = proxyResolver();
// An attacker connecting directly (not through the real proxy) claims to forward from a
// trusted-proxy address itself — since the *immediate peer* is what gates trust, not header
// content, this must still resolve to the attacker's own real peer address.
const result = resolver.resolve({
headers: new Headers({ "x-forwarded-for": "203.0.113.5, 10.0.0.1" }),
peerAddress: "203.0.113.200",
});
expect(result).toEqual({ address: "203.0.113.200", ok: true });
});
});