/** * Protocols that may appear in a link. Everything else (`javascript:`, * `data:`, `vbscript:`, ...) is rejected, because such URLs run * attacker-controlled code in the current origin once a user clicks a * remote-supplied link. */ const ALLOWED_PROTOCOLS = new Set(['http:', 'https:', 'mailto:', 'tel:']); /** * Base used to resolve relative links. It only needs to be a syntactically * valid absolute URL: relative links inherit its scheme, which is why the * value itself never matters. */ const BASE_URL = 'https://uikit.invalid/'; /** * Whether a remote-supplied value is safe to use in a URL attribute * (`href` / `src`). * * The decision is delegated to the platform URL parser instead of a * hand-written regex. The browser is the component that interprets the value * at click time, so reusing its parser guarantees this guard can never drift * from the browser's own normalization: stripping of tab / newline, leading * and trailing control characters, scheme lower-casing, and so on. * * @param value - untrusted value to validate * @returns `true` when the value resolves to an allowed protocol */ export function isSafeUrl(value: unknown): boolean { if (typeof value !== 'string' || value.trim() === '') { return false; } try { return ALLOWED_PROTOCOLS.has(new URL(value, BASE_URL).protocol); } catch { return false; } }