/** * Allowlist-Resolver für `?src=...`-Query-Parameter in den Image- und File-Proxy- * Routen. Verhindert SSRF: Angreifer könnten sonst beliebige URLs an die * CMS-API durchreichen lassen (interne Hosts, Cloud-Metadata-Endpunkte, * Cache-Pollution). * * Erlaubt: * - Asset-Dateiname ("foo.jpg") → {CMS}/api/assets/foo.jpg * - Absoluter Pfad ("/api/assets/...") → {CMS}/api/assets/... * - Vollständige URL exakt unter CMS-Host → unverändert * * Abgelehnt: * - Fremde Hosts, andere Schemata * - Protokoll-relative URLs ("//host/...") * - Pfad-Traversal ("..", führender Punkt) */ import { env } from '$env/dynamic/public'; export function getCmsBaseUrl(): string { return ( env.PUBLIC_CMS_URL || import.meta.env.PUBLIC_CMS_URL || 'http://localhost:3000' ).replace(/\/$/, ''); } export function resolveCmsSource(src: string): string | null { if (src.startsWith('//')) return null; if (src.startsWith('/')) return `${getCmsBaseUrl()}${src}`; if (src.startsWith('http://') || src.startsWith('https://')) { let candidate: URL; let cmsHost: URL; try { candidate = new URL(src); cmsHost = new URL(getCmsBaseUrl()); } catch { return null; } if (candidate.host !== cmsHost.host) return null; if (candidate.protocol !== cmsHost.protocol) return null; return src; } if (src.includes('://') || src.includes('..') || src.startsWith('.')) return null; return `${getCmsBaseUrl()}/api/assets/${encodeURIComponent(src)}`; }