fix(security): Rate-Limits zaehlen IPv6 je /64-Netz
Docker Image bauen und veroeffentlichen / build-and-push (push) Successful in 6m47s

Seit CF-Connecting-IP gilt, sieht der Limiter echte IPv6-Adressen. Ein
Anschluss bekommt meist ein ganzes /64 und koennte durch Adresswechsel
jedes Limit umgehen (Login, 2FA, Checkout). getClientIp liefert IPv6 deshalb
als /64-Praefix, IPv4-gemappte Adressen wie IPv4 (wie VyCept, #94).

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
Nick SzaunigandClaude Opus 5.5 committed 2026-10-12 09:59:09 +02:00
1 parent 946745c064
commit 5a8ff27090
2 files changed
+49 -2

No files matched your search

+16 -1
View File
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { resolveClientIp } from "./rate-limit";
import { rateLimitSubject, resolveClientIp } from "./rate-limit";
const h = (values: Record<string, string>) => new Headers(values);
@@ -23,3 +23,18 @@ describe("resolveClientIp (Client-IP hinter Cloudflare und Traefik)", () => {
expect(resolveClientIp(h({}))).toBe("unknown");
});
});
describe("rateLimitSubject (IPv6 je /64)", () => {
it("fasst alle Adressen eines /64-Netzes zusammen, sonst ließe sich jedes Limit durch Adresswechsel umgehen", () => {
expect(rateLimitSubject("2001:db8:1:2::1")).toBe("2001:db8:1:2::/64");
expect(rateLimitSubject("2001:db8:1:2:aaaa:bbbb:cccc:dddd")).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"));
});
it("lässt IPv4 unverändert und behandelt IPv4-gemappte Adressen wie IPv4", () => {
expect(rateLimitSubject("198.51.100.9")).toBe("198.51.100.9");
expect(rateLimitSubject("::ffff:198.51.100.9")).toBe("198.51.100.9");
expect(rateLimitSubject("unknown")).toBe("unknown");
});
});