fix(security): authorize site uploads and harden tokens, media and request identity
CI / check (push) Successful in 3m15s
CI / deploy (push) Successful in 1m19s
CI / publish-container (push) Successful in 48s

This commit is contained in:
Simo committed 2026-09-13 19:24:43 +02:00
1 parent 52f6d1491f
commit 8abfe352ef
70 files changed
+1609 -204

No files matched your search

+37
View File
@@ -0,0 +1,37 @@
import { isIP } from "node:net";
export const UNKNOWN_CLIENT_IP = "0.0.0.0";
/** Accept bare addresses only, so ports, hostnames and zone IDs cannot become keys. */
export function normalizeClientIp(
value: string | null | undefined,
): string | null {
const address = value?.trim();
if (!address || address.length > 45 || address.includes("%")) return null;
const version = isIP(address);
if (version === 4) return address;
if (version !== 6) return null;
const canonical = new URL(`http://[${address}]/`).hostname.slice(1, -1);
// Treat an IPv4-mapped IPv6 address as the same client as its dotted form.
const mapped = /^::ffff:([a-f0-9]{1,4}):([a-f0-9]{1,4})$/.exec(canonical);
if (mapped) {
const high = Number.parseInt(mapped[1], 16);
const low = Number.parseInt(mapped[2], 16);
return `${high >> 8}.${high & 255}.${low >> 8}.${low & 255}`;
}
return canonical;
}
/**
* Forwarded headers must be overwritten by a trusted ingress and the origin must
* reject direct public access. Header syntax alone cannot establish peer trust.
* Never consume x-real-client-ip: API routes bypass the proxy that once set it.
*/
export function resolveClientIp(headers: Pick<Headers, "get">): string {
return (
normalizeClientIp(headers.get("cf-connecting-ip")) ??
normalizeClientIp(headers.get("x-forwarded-for")?.split(",", 1)[0]) ??
normalizeClientIp(headers.get("x-real-ip")) ??
UNKNOWN_CLIENT_IP
);
}