/** * SSRF guard + client/server agreement for /api/preview/embed. * Only tunnel-like preview hosts should be proxied — never arbitrary URLs. */ export function isPrivateIpHostname(hostname: string): boolean { const m = /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/.exec(hostname); if (!m) return false; const a = Number(m[1]); const b = Number(m[2]); if (a === 10) return true; if (a === 172 && b >= 16 && b <= 31) return true; if (a === 192 && b === 168) return true; if (a === 127) return true; if (a === 169 && b === 254) return true; if (a === 0) return true; return false; } /** Server-side hostname allowlist after redirects */ export function serverPreviewHostnameAllowed(hostname: string, protocol: string): boolean { const h = hostname.toLowerCase(); const p = protocol.toLowerCase(); if (p !== "http:" && p !== "https:") return false; if (isPrivateIpHostname(h)) return false; if (h === "localhost" || h === "127.0.0.1") { return process.env.NODE_ENV === "development"; } if (h.endsWith(".preview.vibnai.com")) return true; if (h === "preview.vibnai.com") return true; const suffixes = (process.env.NEXT_PUBLIC_PREVIEW_EMBED_PROXY_HOST_SUFFIXES ?? "") .split(",") .map((s) => s.trim().toLowerCase()) .filter(Boolean); return suffixes.some((suf) => (suf.startsWith(".") ? h.endsWith(suf) : h === suf)); } /** * Remote URLs we should load through /api/preview/embed so the bridge script is same-origin. * Same-origin targets return false (already works without proxy). */ export function previewUrlEligibleForEmbedProxy(rawUrl: string, appOrigin: string): boolean { try { const u = new URL(rawUrl); const app = new URL(appOrigin); if (u.origin === app.origin) return false; return serverPreviewHostnameAllowed(u.hostname, u.protocol); } catch { return false; } }