Files
Calbook/lib/security/request.ts

119 lines
2.7 KiB
TypeScript

import { fail } from "@/lib/api";
import { getPublicBaseUrl } from "@/lib/public-url";
const DEFAULT_MAX_JSON_BYTES = 64 * 1024;
function parseContentLength(value: string | null): number | null {
if (!value) return null;
const parsed = Number(value);
if (!Number.isFinite(parsed) || parsed < 0) return null;
return Math.floor(parsed);
}
function mutationMethod(method: string) {
const normalized = method.toUpperCase();
return normalized === "POST" || normalized === "PUT" || normalized === "PATCH" || normalized === "DELETE";
}
function isAllowedOrigin(origin: string, req: Request) {
const allowedOrigins = new Set<string>();
try {
allowedOrigins.add(new URL(req.url).origin);
} catch {
// Ignore invalid request URL.
}
try {
allowedOrigins.add(new URL(getPublicBaseUrl()).origin);
} catch {
// Ignore invalid public URL config.
}
if (process.env.NEXTAUTH_URL) {
try {
allowedOrigins.add(new URL(process.env.NEXTAUTH_URL).origin);
} catch {
// Ignore invalid NEXTAUTH_URL config.
}
}
return allowedOrigins.has(origin);
}
export function validateMutationRequestOrigin(req: Request) {
if (!mutationMethod(req.method)) return null;
const fetchSite = (req.headers.get("sec-fetch-site") ?? "").toLowerCase();
if (fetchSite === "cross-site") {
return fail("Ungültige Anfrage-Herkunft", 403);
}
const originHeader = req.headers.get("origin");
if (!originHeader) return null;
let origin: string;
try {
origin = new URL(originHeader).origin;
} catch {
return fail("Ungültige Anfrage-Herkunft", 403);
}
if (!isAllowedOrigin(origin, req)) {
return fail("Ungültige Anfrage-Herkunft", 403);
}
return null;
}
export async function readJsonBody(
req: Request,
options?: { maxBytes?: number }
) {
const maxBytes = options?.maxBytes ?? DEFAULT_MAX_JSON_BYTES;
const contentLength = parseContentLength(req.headers.get("content-length"));
if (contentLength !== null && contentLength > maxBytes) {
return {
ok: false as const,
response: fail("Anfrage ist zu groß", 413)
};
}
let raw = "";
try {
raw = await req.text();
} catch {
return {
ok: false as const,
response: fail("Anfrage konnte nicht gelesen werden", 400)
};
}
if (Buffer.byteLength(raw, "utf8") > maxBytes) {
return {
ok: false as const,
response: fail("Anfrage ist zu groß", 413)
};
}
if (!raw.trim()) {
return {
ok: true as const,
data: {}
};
}
try {
return {
ok: true as const,
data: JSON.parse(raw) as unknown
};
} catch {
return {
ok: false as const,
response: fail("Ungültiges JSON", 400)
};
}
}