From 2a44c9d38469d082c9f53850826caaecf532eae0 Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Sat, 12 Sep 2026 23:06:58 +0200 Subject: [PATCH] refac: validate the URL scheme before rendering a link href (#29890) Link URLs rendered from markdown, citations and web search results now pass a scheme check before they reach an anchor. A new safeLinkUrl helper sits beside isValidHttpUrl and keeps http, https, mailto, tel and relative URLs, returning undefined for anything else so the label renders without a link. Markdown links with an unusual scheme (ftp, sms, and application deep links such as obsidian or vscode) render as plain text from now on. The citation checks move off a substring test for "http" onto isValidHttpUrl, which is what Citations.svelte already uses for the same question. That also drops two long-standing quirks: an uppercase HTTP:// source never rendered as a link, and a filename merely containing "http" rendered as a dead external one. --- .../chat/Messages/Citations/CitationModal.svelte | 11 ++++++----- .../Messages/Markdown/MarkdownInlineTokens.svelte | 7 ++++--- .../ResponseMessage/WebSearchResults.svelte | 6 ++++-- src/lib/utils/index.ts | 14 ++++++++++++++ 4 files changed, 28 insertions(+), 10 deletions(-) diff --git a/src/lib/components/chat/Messages/Citations/CitationModal.svelte b/src/lib/components/chat/Messages/Citations/CitationModal.svelte index 57ea654364..a56e409b27 100644 --- a/src/lib/components/chat/Messages/Citations/CitationModal.svelte +++ b/src/lib/components/chat/Messages/Citations/CitationModal.svelte @@ -6,6 +6,7 @@ import { WEBUI_API_BASE_URL } from '$lib/constants'; import { settings, config } from '$lib/stores'; import { injectCsp } from '$lib/utils/csp'; + import { isValidHttpUrl } from '$lib/utils'; import XMark from '$lib/components/icons/XMark.svelte'; import Textarea from '$lib/components/common/Textarea.svelte'; @@ -71,7 +72,7 @@ const baseUrl = file_id ? `${WEBUI_API_BASE_URL}/files/${file_id}/content${page !== undefined ? `#page=${page + 1}` : ''}` - : sourceUrl?.includes('http') + : isValidHttpUrl(sourceUrl) ? sourceUrl : null; @@ -101,10 +102,10 @@
{#if citation?.source?.name} {@const document = mergedDocuments?.[0]} - {#if document?.metadata?.file_id || document.source?.url?.includes('http')} + {#if document?.metadata?.file_id || isValidHttpUrl(document.source?.url)} - {#if document.source?.url?.includes('http')} + {#if isValidHttpUrl(document.source?.url)} {@const snippetUrl = getTextFragmentUrl(document)} {#if snippetUrl} {:else if token.type === 'link'} {@const noteId = getNoteIdFromHref(token.href)} + {@const safeHref = safeLinkUrl(token.href)} {#if noteId} {:else if token.tokens} {:else} + import { safeLinkUrl } from '$lib/utils'; + import ChevronDown from '$lib/components/icons/ChevronDown.svelte'; import ChevronUp from '$lib/components/icons/ChevronUp.svelte'; import Search from '$lib/components/icons/Search.svelte'; @@ -59,7 +61,7 @@ {#if status?.items} {#each status.items as item, itemIdx} @@ -99,7 +101,7 @@ {:else if status?.urls} {#each status.urls as url, urlIdx} diff --git a/src/lib/utils/index.ts b/src/lib/utils/index.ts index 91b66f8524..7073503aa9 100644 --- a/src/lib/utils/index.ts +++ b/src/lib/utils/index.ts @@ -981,6 +981,20 @@ export const isValidHttpUrl = (string: string) => { return url.protocol === 'http:' || url.protocol === 'https:'; }; +const SAFE_LINK_PROTOCOLS = ['http:', 'https:', 'mailto:', 'tel:']; + +export const safeLinkUrl = (url: string): string | undefined => { + let protocol; + try { + protocol = new URL(url).protocol; + } catch (_) { + // No scheme to parse, so the browser resolves it against our own origin. + return url; + } + + return SAFE_LINK_PROTOCOLS.includes(protocol) ? url : undefined; +}; + export const isYoutubeUrl = (url: string) => { return ( url.startsWith('https://www.youtube.com') ||