Docker-Images bauen und veroeffentlichen / build-and-push (push) Successful in 8m8s
- CF-Connecting-IP gilt nur noch als oeffentliche Unicast-Adresse (nicht 0/8, 10/8, 100.64/10, 127/8, 169.254/16, 172.16/12, 192.168/16, 240/4, Broadcast, ::, ::1, fc00::/7, fe80::/10, ::ffff:privat); sonst gilt die Gegenstelle. - X-Real-IP ohne X-Forwarded-For nur noch ausserhalb von Produktion; in Produktion ergibt das "unknown". - Neuer Helfer headersWithClientIp, von withClientIp und der Admin-Session-Pruefung genutzt: ein vom Client gesendeter x-artista-client-ip-Wert wird nie geglaubt. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
89 lines
4.5 KiB
TypeScript
89 lines
4.5 KiB
TypeScript
import { afterEach, describe, expect, test, vi } from "vitest";
|
|
import { CLIENT_IP_HEADER, clientIp, headersWithClientIp, rateLimitSubject, withClientIp } from "@/lib/rate-limit";
|
|
|
|
const h = (values: Record<string, string>) => new Headers(values);
|
|
|
|
describe("clientIp hinter Traefik", () => {
|
|
test("Gegenstelle ist der letzte Eintrag in X-Forwarded-For, nie ein Eintrag des Clients", () => {
|
|
expect(clientIp(h({ "x-forwarded-for": "1.2.3.4, 198.51.100.9", "x-real-ip": "1.2.3.4" }))).toBe("198.51.100.9");
|
|
});
|
|
|
|
test("über Cloudflare gefälschtes X-Real-IP zählt nicht, CF-Connecting-IP nur von Cloudflare-Edges", () => {
|
|
const viaCloudflare = { "x-real-ip": "1.2.3.4", "x-forwarded-for": "1.2.3.4, 203.0.113.7, 172.70.1.2", "cf-connecting-ip": "203.0.113.7" };
|
|
expect(clientIp(h(viaCloudflare))).toBe("203.0.113.7");
|
|
expect(clientIp(h({ "x-forwarded-for": "198.51.100.9", "cf-connecting-ip": "203.0.113.7" }))).toBe("198.51.100.9");
|
|
});
|
|
|
|
test("ohne X-Forwarded-For (lokal) gilt X-Real-IP, ungültige Werte ergeben „unknown“", () => {
|
|
expect(clientIp(h({ "x-real-ip": "198.51.100.9" }))).toBe("198.51.100.9");
|
|
expect(clientIp(h({}))).toBe("unknown");
|
|
expect(clientIp(h({ "x-forwarded-for": "1.2.3.4, kein-ip" }))).toBe("unknown");
|
|
});
|
|
|
|
test("Rate-Limits zählen IPv6 je /64-Netz, IPv4-gemappte Adressen wie IPv4", () => {
|
|
expect(rateLimitSubject(clientIp(h({ "x-forwarded-for": "2001:db8:1:2:aaaa::1" })))).toBe("2001:db8:1:2::/64");
|
|
expect(rateLimitSubject("2001:0DB8:0001:0002:ffff:ffff:ffff:ffff")).toBe("2001:db8:1:2::/64");
|
|
expect(rateLimitSubject("2001:db8:1:3::1")).not.toBe(rateLimitSubject("2001:db8:1:2::1"));
|
|
expect(rateLimitSubject("::ffff:198.51.100.9")).toBe("198.51.100.9");
|
|
expect(rateLimitSubject("198.51.100.9")).toBe("198.51.100.9");
|
|
});
|
|
|
|
test("better-auth bekommt eine einzelne gültige IP (es normalisiert IPv6 selbst auf /64)", () => {
|
|
const request = new Request("https://example.org/api/auth/sign-in/email", { headers: { "x-forwarded-for": "2001:db8:1:2:aaaa::1" } });
|
|
expect(withClientIp(request).headers.get(CLIENT_IP_HEADER)).toBe("2001:db8:1:2:aaaa::1");
|
|
});
|
|
|
|
test("withClientIp setzt den Header für better-auth und überschreibt einen mitgeschickten Wert", () => {
|
|
const request = new Request("https://example.org/api/auth/sign-in/email", {
|
|
method: "POST",
|
|
headers: { "x-forwarded-for": "1.2.3.4, 198.51.100.9", [CLIENT_IP_HEADER]: "1.2.3.4" },
|
|
body: "{}",
|
|
});
|
|
expect(withClientIp(request).headers.get(CLIENT_IP_HEADER)).toBe("198.51.100.9");
|
|
});
|
|
});
|
|
|
|
describe("CF-Connecting-IP nur als öffentliche Unicast-Adresse", () => {
|
|
const edge = "172.70.1.2";
|
|
const viaEdge = (cf: string) => clientIp(h({ "x-forwarded-for": `1.2.3.4, ${edge}`, "cf-connecting-ip": cf }));
|
|
|
|
test.each([
|
|
"0.1.2.3", "10.0.0.5", "100.64.0.1", "100.127.255.255", "127.0.0.1", "169.254.1.1", "172.16.0.1", "172.31.255.255",
|
|
"192.168.1.1", "240.0.0.1", "255.255.255.255", "::", "::1", "fc00::1", "fd12::1", "fe80::1", "::ffff:10.0.0.1", "::ffff:192.168.0.1",
|
|
])("%s wird verworfen, die Gegenstelle gilt", (cf) => {
|
|
expect(viaEdge(cf)).toBe(edge);
|
|
});
|
|
|
|
test.each(["203.0.113.7", "100.63.255.255", "172.32.0.1", "2001:db8::1", "::ffff:203.0.113.7"])("%s wird übernommen", (cf) => {
|
|
expect(viaEdge(cf)).toBe(cf);
|
|
});
|
|
});
|
|
|
|
describe("X-Real-IP in Produktion", () => {
|
|
afterEach(() => vi.unstubAllEnvs());
|
|
|
|
test("ohne X-Forwarded-For ergibt X-Real-IP in Produktion „unknown“", () => {
|
|
vi.stubEnv("NODE_ENV", "production");
|
|
expect(clientIp(h({ "x-real-ip": "198.51.100.9" }))).toBe("unknown");
|
|
expect(clientIp(h({ "x-forwarded-for": "198.51.100.9" }))).toBe("198.51.100.9");
|
|
});
|
|
|
|
test("außerhalb von Produktion gilt X-Real-IP weiter", () => {
|
|
vi.stubEnv("NODE_ENV", "development");
|
|
expect(clientIp(h({ "x-real-ip": "198.51.100.9" }))).toBe("198.51.100.9");
|
|
});
|
|
});
|
|
|
|
describe("headersWithClientIp", () => {
|
|
test("überschreibt einen mitgeschickten Wert und lässt das Original unverändert", () => {
|
|
const original = h({ "x-forwarded-for": "1.2.3.4, 198.51.100.9", [CLIENT_IP_HEADER]: "1.2.3.4" });
|
|
const result = headersWithClientIp(original);
|
|
expect(result.get(CLIENT_IP_HEADER)).toBe("198.51.100.9");
|
|
expect(original.get(CLIENT_IP_HEADER)).toBe("1.2.3.4");
|
|
});
|
|
|
|
test("ohne ermittelbare IP steht „unknown“ im Header, nie der Clientwert", () => {
|
|
expect(headersWithClientIp(h({ [CLIENT_IP_HEADER]: "1.2.3.4" })).get(CLIENT_IP_HEADER)).toBe("unknown");
|
|
});
|
|
});
|