import { get } from 'svelte/store' import { hubBaseUrlStore } from './stores' export function isValidLogoutRedirect(url: string): boolean { if (url.startsWith('/') && !url.startsWith('//')) { return true } try { const parsed = new URL(url) // Same-origin absolute URLs are always safe: the user can only be sent // back to the site they're already on, which is never an open redirect. // Mirrors toSameOriginRelativePath()'s same-origin handling, and lets a // self-hosted instance redirect back to its own app after OAuth login. if (typeof window !== 'undefined' && parsed.origin === window.location.origin) { return true } const host = parsed.hostname if (host === 'windmill.dev' || host.endsWith('.windmill.dev')) { return true } const hubBaseUrl = get(hubBaseUrlStore) try { const hubHost = new URL(hubBaseUrl).hostname if (host === hubHost) { return true } } catch {} } catch {} return false } /** * Reduces a redirect target to a safe same-origin relative path. * * Returns the path (`/foo?bar#baz`) when the input is either: * - already an absolute-but-relative path (`/foo`) that isn't protocol-relative * (`//evil.com`) or a back-slash trick (`/\\evil.com`), or * - a full URL whose origin matches the current page's origin. * * Returns `null` for anything else (cross-origin URLs, protocol-relative paths, * malformed input). Used to sanitize values flowing into `RelayState`, * `localStorage.rd`, and post-login redirects so we don't either lose a valid * same-origin deep link or open-redirect into a foreign origin. */ export function toSameOriginRelativePath(rd: string | null | undefined): string | null { if (!rd) return null if (rd.length > 2048) return null if (hasControlChar(rd)) return null if (rd.startsWith('/')) { if (rd.startsWith('//') || rd.startsWith('/\\')) return null return rd } if (typeof window === 'undefined') return null try { const url = new URL(rd, window.location.origin) if (url.origin !== window.location.origin) return null const path = url.pathname + url.search + url.hash return path || '/' } catch { return null } } function hasControlChar(s: string): boolean { for (let i = 0; i < s.length; i++) { const c = s.charCodeAt(i) if (c < 0x20 || c === 0x7f) return true } return false }