Files
florian.wilde 0958abaac6 Sicherheit: SVG-Upload für Logo/Favicon nicht mehr erlaubt
Der Branding-Upload akzeptierte bisher jeden Dateityp, der clientseitig
mit "image/" beginnt, inkl. SVG. Anders als beim vorherigen Supabase-
Storage-Bucket (andere Origin) wird die Datei jetzt auf der eigenen
Origin unter /branding/[filename] ausgeliefert – eine SVG mit
eingebettetem <script> hätte bei direktem Aufruf der Datei-URL
(z. B. "Bild in neuem Tab öffnen") mit dem Session-Cookie eines
anderen Admins ausgeführt werden können und wäre dabei auch am
CSRF-Schutz vorbeigekommen (Origin stimmt ja).

Upload-Route erlaubt jetzt nur noch eine Whitelist an Rasterformaten
(png/jpg/webp/gif/ico), die Dateiendung wird aus dem geprüften
MIME-Typ abgeleitet statt aus dem vom Client gesendeten Dateinamen.
Die Ausliefer-Route kennt "svg" als Content-Type nicht mehr (Defense
in Depth). Gefunden bei der Sicherheitsprüfung vor dem Merge nach
master.
2026-07-18 11:58:07 +02:00

37 lines
1.3 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { readBrandingFile } from "@/lib/branding";
// Kein svg: würde mit Content-Type image/svg+xml ausgeliefert und ist
// bei direkter Navigation auf die Datei aktiv ausführbar (eingebettetes
// <script> läuft dann same-origin inkl. Session-Cookie) – anders als bei
// Einbettung per <img>/<link rel="icon">. Nur ungefährliche Rasterformate
// zulassen, siehe auch die Whitelist beim Upload in api/admin/branding.
const CONTENT_TYPES: Record<string, string> = {
png: "image/png",
jpg: "image/jpeg",
jpeg: "image/jpeg",
webp: "image/webp",
ico: "image/x-icon",
gif: "image/gif",
};
export async function GET(
request: Request,
{ params }: { params: Promise<{ filename: string }> }
) {
const { filename } = await params;
const data = await readBrandingFile(filename);
if (!data) return new Response("Not found", { status: 404 });
const ext = filename.split(".").pop()?.toLowerCase() ?? "";
const contentType = CONTENT_TYPES[ext] ?? "application/octet-stream";
return new Response(new Uint8Array(data), {
headers: {
"Content-Type": contentType,
// Dateinamen tragen bereits einen Date.now()-Suffix, ein neuer
// Upload bekommt also automatisch einen neuen Namen.
"Cache-Control": "public, max-age=31536000, immutable",
},
});
}