···6363}
64646565/**
6666+ * Normalize a configured pattern to a bare host fragment, stripping any scheme,
6767+ * port, or path so legacy full-URL configs keep working. Dotless fragments are
6868+ * returned as-is for substring matching.
6969+ */
7070+function normalizePatternHost(pattern: string): string {
7171+ const trimmed = pattern.trim().toLowerCase();
7272+ if (trimmed.length === 0) return '';
7373+ let host = trimmed;
7474+ if (/[:/]/.test(trimmed)) {
7575+ try {
7676+ host = new URL(trimmed.includes('://') ? trimmed : `http://${trimmed}`)
7777+ .hostname;
7878+ } catch {
7979+ // Not parseable as a URL; fall back to the raw fragment.
8080+ }
8181+ }
8282+ return host.replace(/^\.+|\.+$/g, '');
8383+}
8484+8585+/**
8686+ * Check whether a single pattern matches a hostname.
8787+ *
8888+ * Domain-like patterns (those containing a dot, e.g. `example.com`) are matched
8989+ * on host boundaries so a pattern can't be smuggled via a lookalike or subdomain
9090+ * (`notexample.com` and `example.com.evil.test` do NOT match `example.com`).
9191+ * Legacy patterns without a dot keep substring matching for backward
9292+ * compatibility with existing `VITE_CONTENT_URL_PATTERN` configs.
9393+ */
9494+function hostnameMatchesPattern(hostname: string, pattern: string): boolean {
9595+ const normalized = normalizePatternHost(pattern);
9696+ if (normalized.length === 0) return false;
9797+ if (normalized.includes('.')) {
9898+ return hostname === normalized || hostname.endsWith(`.${normalized}`);
9999+ }
100100+ return hostname.includes(normalized);
101101+}
102102+103103+/**
66104 * Check if a URL should be displayed in an iframe
105105+ *
106106+ * Patterns are matched against the URL's hostname only (not the full URL), so a
107107+ * pattern can't be smuggled in via the path or query string (e.g.
108108+ * `https://evil.test/?ref=example.com` does not match the `example.com` pattern).
109109+ * Domain-like patterns are further matched on host boundaries; see
110110+ * {@link hostnameMatchesPattern}.
67111 * @param url The URL to check
68112 * @returns True if the URL should be displayed in an iframe
69113 */
70114export function shouldDisplayInIframe(url: string): boolean {
115115+ let hostname: string;
116116+ try {
117117+ hostname = new URL(url).hostname.toLowerCase();
118118+ } catch {
119119+ return false;
120120+ }
71121 const patterns = getContentUrlPatterns();
7272- const urlLower = url.toLowerCase();
7373- return patterns.some((pattern) => urlLower.includes(pattern.toLowerCase()));
122122+ return patterns.some((pattern) => hostnameMatchesPattern(hostname, pattern));
74123}
7512476125/**