fix: Client-IP haerten (CF-Connecting-IP, X-Real-IP, Admin-Session)
Docker-Images bauen und veroeffentlichen / build-and-push (push) Successful in 8m8s
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]>
This commit is contained in:
1 parent
34ccbc3b9a
commit
5a88f3dcd6
3 files changed
+79
-8
No files matched your search
@@ -7,6 +7,7 @@ import { headers } from "next/headers";
|
||||
import { redirect } from "next/navigation";
|
||||
import type { Actor } from "./audit";
|
||||
import { getAuth } from "./auth";
|
||||
import { headersWithClientIp } from "./rate-limit";
|
||||
|
||||
export type AdminUser = { id: string; email: string; name: string; twoFactorEnabled: boolean };
|
||||
|
||||
@@ -15,7 +16,7 @@ async function currentAdmin(): Promise<AdminUser | null> {
|
||||
// Sonst versucht "next build" sie vorab zu rendern und scheitert ohne
|
||||
// Secrets im Container (getAuth() braucht BETTER_AUTH_SECRET).
|
||||
const requestHeaders = await headers();
|
||||
const session = await getAuth().api.getSession({ headers: requestHeaders });
|
||||
const session = await getAuth().api.getSession({ headers: headersWithClientIp(requestHeaders) });
|
||||
if (!session) return null;
|
||||
const user = session.user as typeof session.user & { twoFactorEnabled?: boolean | null };
|
||||
return { id: user.id, email: user.email, name: user.name, twoFactorEnabled: Boolean(user.twoFactorEnabled) };
|
||||
|
||||
+31
-5
@@ -67,6 +67,26 @@ const CLOUDFLARE: [string, number, "ipv4" | "ipv6"][] = [
|
||||
const cloudflare = new BlockList();
|
||||
for (const [net, prefix, family] of CLOUDFLARE) cloudflare.addSubnet(net, prefix, family);
|
||||
|
||||
/** Nicht oeffentliche Bereiche; 240/4 nutzt Cloudflare fuer "Pseudo IPv4". */
|
||||
const NON_PUBLIC_V4 = ["0.0.0.0/8", "10.0.0.0/8", "100.64.0.0/10", "127.0.0.0/8", "169.254.0.0/16", "172.16.0.0/12", "192.168.0.0/16", "240.0.0.0/4"];
|
||||
const nonPublic = new BlockList();
|
||||
for (const cidr of NON_PUBLIC_V4) nonPublic.addSubnet(cidr.split("/")[0]!, Number(cidr.split("/")[1]), "ipv4");
|
||||
nonPublic.addAddress("255.255.255.255", "ipv4");
|
||||
nonPublic.addAddress("::", "ipv6");
|
||||
nonPublic.addAddress("::1", "ipv6");
|
||||
nonPublic.addSubnet("fc00::", 7, "ipv6");
|
||||
nonPublic.addSubnet("fe80::", 10, "ipv6");
|
||||
|
||||
function isPublicUnicast(ip: string): boolean {
|
||||
if (isIP(ip) === 4) return !nonPublic.check(ip, "ipv4");
|
||||
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 !nonPublic.check([high >> 8, high & 0xff, low >> 8, low & 0xff].join("."), "ipv4");
|
||||
}
|
||||
return !nonPublic.check(ip, "ipv6");
|
||||
}
|
||||
|
||||
function validIp(value: string | null | undefined): string | null {
|
||||
const ip = value?.trim();
|
||||
return ip && isIP(ip) ? ip : null;
|
||||
@@ -82,10 +102,11 @@ function validIp(value: string | null | undefined): string | null {
|
||||
*/
|
||||
export function clientIp(headers: Headers): string {
|
||||
const forwarded = headers.get("x-forwarded-for");
|
||||
const peer = forwarded === null ? validIp(headers.get("x-real-ip")) : validIp(forwarded.split(",").at(-1));
|
||||
const realIp = process.env.NODE_ENV === "production" ? null : validIp(headers.get("x-real-ip"));
|
||||
const peer = forwarded === null ? realIp : validIp(forwarded.split(",").at(-1));
|
||||
if (!peer) return "unknown";
|
||||
const viaCloudflare = validIp(headers.get("cf-connecting-ip"));
|
||||
return viaCloudflare && cloudflare.check(peer, isIP(peer) === 6 ? "ipv6" : "ipv4") ? viaCloudflare : peer;
|
||||
return viaCloudflare && isPublicUnicast(viaCloudflare) && cloudflare.check(peer, isIP(peer) === 6 ? "ipv6" : "ipv4") ? viaCloudflare : peer;
|
||||
}
|
||||
|
||||
/** Die acht 16-Bit-Gruppen einer gueltigen IPv6-Adresse; eine IPv4-Endung (::ffff:a.b.c.d) wird umgerechnet. */
|
||||
@@ -119,9 +140,14 @@ export function rateLimitSubject(ip: string): string {
|
||||
return `${groups.slice(0, 4).map((group) => group.toString(16)).join(":")}::/64`;
|
||||
}
|
||||
|
||||
/** Kopie der Header mit CLIENT_IP_HEADER aus clientIp(); ein mitgeschickter Wert wird ersetzt. */
|
||||
export function headersWithClientIp(source: Headers): Headers {
|
||||
const headers = new Headers(source);
|
||||
headers.set(CLIENT_IP_HEADER, clientIp(source));
|
||||
return headers;
|
||||
}
|
||||
|
||||
/** Kopie der Anfrage mit CLIENT_IP_HEADER aus clientIp(); ein mitgeschickter Wert wird ersetzt. */
|
||||
export function withClientIp(request: Request): Request {
|
||||
const headers = new Headers(request.headers);
|
||||
headers.set(CLIENT_IP_HEADER, clientIp(request.headers));
|
||||
return new Request(request, { headers });
|
||||
return new Request(request, { headers: headersWithClientIp(request.headers) });
|
||||
}
|
||||
Reference in new issue
Block a user