mirror of
https://github.com/open-webui/open-webui.git
synced 2026-09-12 23:02:35 +00:00
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.
This commit is contained in:
parent
746caa7c78
commit
2a44c9d384
4 changed files with 28 additions and 10 deletions
|
|
@ -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 @@
|
|||
<div class=" text-sm font-medium self-center flex items-center">
|
||||
{#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)}
|
||||
<Tooltip
|
||||
className="w-fit"
|
||||
content={document.source?.url?.includes('http')
|
||||
content={isValidHttpUrl(document.source?.url)
|
||||
? $i18n.t('Open link')
|
||||
: $i18n.t('Open file')}
|
||||
placement="top-start"
|
||||
|
|
@ -114,7 +115,7 @@
|
|||
class="hover:text-gray-500 dark:hover:text-gray-100 underline grow line-clamp-1"
|
||||
href={document?.metadata?.file_id
|
||||
? `${WEBUI_API_BASE_URL}/files/${document?.metadata?.file_id}/content${document?.metadata?.page !== undefined ? `#page=${document.metadata.page + 1}` : ''}`
|
||||
: document.source?.url?.includes('http')
|
||||
: isValidHttpUrl(document.source?.url)
|
||||
? document.source.url
|
||||
: `#`}
|
||||
target="_blank"
|
||||
|
|
@ -161,7 +162,7 @@
|
|||
<div
|
||||
class=" text-sm font-normal dark:text-gray-300 flex items-center gap-2 w-fit mb-1"
|
||||
>
|
||||
{#if document.source?.url?.includes('http')}
|
||||
{#if isValidHttpUrl(document.source?.url)}
|
||||
{@const snippetUrl = getTextFragmentUrl(document)}
|
||||
{#if snippetUrl}
|
||||
<a
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
const i18n = getContext('i18n');
|
||||
|
||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||
import { copyToClipboard, unescapeHtml } from '$lib/utils';
|
||||
import { copyToClipboard, safeLinkUrl, unescapeHtml } from '$lib/utils';
|
||||
|
||||
import Image from '$lib/components/common/Image.svelte';
|
||||
import KatexRenderer from './KatexRenderer.svelte';
|
||||
|
|
@ -74,11 +74,12 @@
|
|||
<HtmlToken {id} {token} {onSourceClick} />
|
||||
{:else if token.type === 'link'}
|
||||
{@const noteId = getNoteIdFromHref(token.href)}
|
||||
{@const safeHref = safeLinkUrl(token.href)}
|
||||
{#if noteId}
|
||||
<NoteLinkToken {noteId} href={token.href} />
|
||||
{:else if token.tokens}
|
||||
<a
|
||||
href={token.href}
|
||||
href={safeHref}
|
||||
target="_blank"
|
||||
rel="nofollow"
|
||||
title={token.title}
|
||||
|
|
@ -88,7 +89,7 @@
|
|||
</a>
|
||||
{:else}
|
||||
<a
|
||||
href={token.href}
|
||||
href={safeHref}
|
||||
target="_blank"
|
||||
rel="nofollow"
|
||||
title={token.title}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
<script lang="ts">
|
||||
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}
|
||||
<a
|
||||
href={item.link}
|
||||
href={safeLinkUrl(item.link)}
|
||||
target="_blank"
|
||||
class="flex w-full items-center p-1 px-3 group/item justify-between text-gray-800 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-850 rounded-lg font-normal! no-underline! mb-1"
|
||||
>
|
||||
|
|
@ -99,7 +101,7 @@
|
|||
{:else if status?.urls}
|
||||
{#each status.urls as url, urlIdx}
|
||||
<a
|
||||
href={url}
|
||||
href={safeLinkUrl(url)}
|
||||
target="_blank"
|
||||
class="flex w-full items-center p-1 px-3 group/item justify-between text-gray-800 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-850 rounded-lg no-underline mb-1"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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') ||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue