/** * Small, dependency-free security helpers shared across packages. */ const HTML_ESCAPES: Record = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'", }; /** * Escape a string for safe interpolation into HTML text or attributes. * Used for page metadata (title/description) so untrusted values can't * break out of an attribute or inject markup. */ export function escapeHtml(value: string): string { return value.replace(/[&<>"']/g, (ch) => HTML_ESCAPES[ch]!); } /** * Client island names come from `data-client="..."` attributes and from * filenames in `app/client`. We only ever allow a conservative charset so a * name can never be used to traverse the filesystem or inject code. */ const SAFE_NAME = /^[A-Za-z0-9_-]+$/; export function isSafeIslandName(name: string): boolean { return SAFE_NAME.test(name); } /** * Reject obvious path-traversal in a request path before it is ever used to * resolve a file. The router never builds file paths from request input * (routes are resolved against a pre-scanned table), but this is a cheap * defense-in-depth guard. */ export function isSafeRequestPath(pathname: string): boolean { if (pathname.includes("\0")) return false; // Reject `..` segments and backslashes that could escape a directory. const decoded = safeDecode(pathname); if (decoded === null) return false; return !/(^|\/)\.\.(\/|$)/.test(decoded) && !decoded.includes("\\"); } function safeDecode(value: string): string | null { try { return decodeURIComponent(value); } catch { return null; } }