/** * Sanitizes a user-supplied URL to prevent phishing and script injection. * * Rules: * - Only emits https:// URLs. `javascript:`, `data:`, `vbscript:`, `file:`, * relative paths, and every other scheme are rejected (returns null). * - Bare `http://` links are UPGRADED to `https://` rather than dropped, so we * don't silently lose links that work fine over TLS. Plain `http` is a * downgrade / mixed-content / IP-leak vector and also fails outright inside * the native (Capacitor) HTTPS-only WebView, so we never emit it. * - Returns null for invalid / unsafe URLs (caller should hide the link). * * VPN/Tor users are unaffected — this validates the URL scheme & structure, * not the user's network origin. */ export function sanitizeUrl(url: string | undefined | null): string | null { if (!url) return null; const trimmed = url.trim(); if (!trimmed) return null; try { const parsed = new URL(trimmed); if (parsed.protocol === 'https:') { return parsed.href; } // Upgrade http → https instead of dropping the link outright. if (parsed.protocol === 'http:') { parsed.protocol = 'https:'; return parsed.href; } // Everything else (javascript:, data:, file:, etc.) is unsafe. return null; } catch { // Not a valid absolute URL (also rejects relative paths — intentional, // since untrusted relative URLs can trigger same-origin requests). return null; } } /** * Allowlists safe characters for interpolating an untrusted string into a CSS * value (font-family, animation-name, etc.). Keeps Unicode letters/numbers, * spaces, hyphens, underscores, apostrophes and periods; strips everything * that could break out of the CSS string context (`"`, `)`, `}`, `;`, `\`). * * URLs in `url(...)` must use `sanitizeUrl()` instead — the URL constructor * percent-encodes the dangerous characters and rejects non-http(s) schemes. */ export function sanitizeCssString(value: string | undefined | null): string { if (!value) return ''; return value.replace(/[^\p{L}\p{N} _\-'.]/gu, ''); } /** * Extracts just the hostname for display (e.g. "Opens: example.com") */ export function urlDomain(url: string): string { try { return new URL(url).hostname.replace(/^www\./, ''); } catch { return url; } } /** * Sanitizes a tel: or mailto: href. * Only allows the specific scheme, strips anything else. */ export function sanitizeTel(value: string | undefined | null): string | null { if (!value) return null; const trimmed = value.trim(); // Allow digits, +, -, spaces, parentheses only const digits = trimmed.replace(/[^0-9+\-\s().]/g, ''); return digits.length >= 5 ? `tel:${digits}` : null; } export function sanitizeEmail(value: string | undefined | null): string | null { if (!value) return null; const trimmed = value.trim(); // Basic email pattern if (/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(trimmed)) { return `mailto:${trimmed}`; } return null; }