fix(security): Rate-Limits zaehlen IPv6 je /64-Netz
Docker Image bauen und veroeffentlichen / build-and-push (push) Successful in 6m47s
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:
1 parent
946745c064
commit
5a8ff27090
2 files changed
+49
-2
No files matched your search
@@ -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");
|
||||
});
|
||||
});
|
||||
+33
-1
@@ -49,7 +49,39 @@ export function rateLimit(key: string, limit: number, windowMs: number): boolean
|
||||
* Rate-Limits in dieser Datei faelschen (Login, 2FA, Checkout, Reset, ...).
|
||||
*/
|
||||
export async function getClientIp(): Promise<string> {
|
||||
return resolveClientIp(await headers());
|
||||
return rateLimitSubject(resolveClientIp(await headers()));
|
||||
}
|
||||
|
||||
/** Die acht 16-Bit-Gruppen einer gueltigen IPv6-Adresse; eine IPv4-Endung (::ffff:a.b.c.d) wird umgerechnet. */
|
||||
function ipv6Groups(ip: string): number[] {
|
||||
let text = ip.toLowerCase().split("%")[0] ?? "";
|
||||
const dotted = /(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(text);
|
||||
if (dotted) {
|
||||
const [a = 0, b = 0, c = 0, d = 0] = dotted.slice(1).map(Number);
|
||||
text = `${text.slice(0, dotted.index)}${((a << 8) | b).toString(16)}:${((c << 8) | d).toString(16)}`;
|
||||
}
|
||||
const parse = (part: string) => (part ? part.split(":").map((group) => Number.parseInt(group, 16)) : []);
|
||||
const [head = "", tail] = text.split("::");
|
||||
const left = parse(head);
|
||||
if (tail === undefined) return left;
|
||||
const right = parse(tail);
|
||||
return [...left, ...new Array<number>(8 - left.length - right.length).fill(0), ...right];
|
||||
}
|
||||
|
||||
/**
|
||||
* Schluessel fuer Rate-Limits: IPv4 unveraendert, IPv6 als /64-Netz. Ein
|
||||
* Anschluss bekommt meist ein ganzes /64; einzeln gezaehlt liesse sich jedes
|
||||
* Limit durch Wechsel der Adresse innerhalb des eigenen Netzes umgehen.
|
||||
* IPv4-gemappte Adressen zaehlen wie die IPv4-Adresse selbst.
|
||||
*/
|
||||
export function rateLimitSubject(ip: string): string {
|
||||
if (isIP(ip) !== 6) return ip;
|
||||
const groups = ipv6Groups(ip);
|
||||
if (groups.slice(0, 5).every((group) => group === 0) && groups[5] === 0xffff) {
|
||||
const [high = 0, low = 0] = groups.slice(6);
|
||||
return [high >> 8, high & 0xff, low >> 8, low & 0xff].join(".");
|
||||
}
|
||||
return `${groups.slice(0, 4).map((group) => group.toString(16)).join(":")}::/64`;
|
||||
}
|
||||
|
||||
/** https://www.cloudflare.com/ips/ (Stand 2026-10). */
|
||||
|
||||
Reference in new issue
Block a user