fix:image url validation and signout post (#24420)
* refac(routers): reject external URLs in profile/model image handlers * refac(ui): centralize image URL validation in safeImageUrl helper * refac(auths): make signout POST-only * refac: gate external profile image redirect behind ENABLE_PROFILE_IMAGE_URL_FORWARDING Restore the 302 redirect for external http(s) profile image URLs in the user and model profile-image endpoints, but gate it behind a new ENABLE_PROFILE_IMAGE_URL_FORWARDING env flag (default: True). Existing deployments that rely on external profile image forwarding continue to work unchanged. Operators who want to suppress the redirect (to prevent client-side IP/UA/Referer leaks) can set the flag to False.
This commit is contained in:
@@ -328,7 +328,7 @@ export const userSignOut = async () => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/auths/signout`, {
|
||||
method: 'GET',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||
import { safeImageUrl } from '$lib/utils/safeImageUrl';
|
||||
|
||||
export let className = 'size-8';
|
||||
export let src = `${WEBUI_BASE_URL}/static/favicon.png`;
|
||||
@@ -7,14 +8,7 @@
|
||||
|
||||
<img
|
||||
aria-hidden="true"
|
||||
src={src === ''
|
||||
? `${WEBUI_BASE_URL}/static/favicon.png`
|
||||
: src.startsWith(WEBUI_BASE_URL) ||
|
||||
src.startsWith('https://www.gravatar.com/avatar/') ||
|
||||
src.startsWith('data:') ||
|
||||
src.startsWith('/')
|
||||
? src
|
||||
: `${WEBUI_BASE_URL}/user.png`}
|
||||
src={safeImageUrl(src)}
|
||||
class=" {className} object-cover rounded-full"
|
||||
alt="profile"
|
||||
draggable="false"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||
import { safeImageUrl } from '$lib/utils/safeImageUrl';
|
||||
|
||||
import { settings } from '$lib/stores';
|
||||
import ImagePreview from './ImagePreview.svelte';
|
||||
@@ -19,7 +20,7 @@
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
let _src = '';
|
||||
$: _src = src.startsWith('/') ? `${WEBUI_BASE_URL}${src}` : src;
|
||||
$: _src = safeImageUrl(src.startsWith('/') ? `${WEBUI_BASE_URL}${src}` : src);
|
||||
|
||||
let showImagePreview = false;
|
||||
</script>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { mergeAttributes, Node, nodeInputRule } from '@tiptap/core';
|
||||
import { safeImageUrl } from '$lib/utils/safeImageUrl';
|
||||
|
||||
export interface ImageOptions {
|
||||
/**
|
||||
@@ -137,12 +138,12 @@ export const Image = Node.create<ImageOptions>({
|
||||
if (editorFiles && node.attrs.src.startsWith('data://')) {
|
||||
const file = editorFiles.find((f) => f.id === fileId);
|
||||
if (file) {
|
||||
img.setAttribute('src', file.url || '');
|
||||
img.setAttribute('src', safeImageUrl(file.url || ''));
|
||||
} else {
|
||||
img.setAttribute('src', '/image-placeholder.png');
|
||||
}
|
||||
} else {
|
||||
img.setAttribute('src', node.attrs.src || '');
|
||||
img.setAttribute('src', safeImageUrl(node.attrs.src || ''));
|
||||
}
|
||||
|
||||
img.setAttribute('alt', node.attrs.alt || '');
|
||||
@@ -153,7 +154,7 @@ export const Image = Node.create<ImageOptions>({
|
||||
if (files && node.attrs.src.startsWith('data://')) {
|
||||
const file = editorFiles.find((f) => f.id === fileId);
|
||||
if (file) {
|
||||
img.setAttribute('src', file.url || '');
|
||||
img.setAttribute('src', safeImageUrl(file.url || ''));
|
||||
} else {
|
||||
img.setAttribute('src', '/image-placeholder.png');
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||
|
||||
const PLACEHOLDER_IMAGE = '/favicon.png';
|
||||
|
||||
/**
|
||||
* Validates an image URL against an allowlist of safe patterns and returns
|
||||
* the URL if trusted, or a placeholder otherwise.
|
||||
*
|
||||
* Allowed patterns:
|
||||
* - Relative paths (starting with '/')
|
||||
* - data:image/* URIs
|
||||
* - Same-origin URLs (starting with WEBUI_BASE_URL)
|
||||
* - Gravatar URLs (https://www.gravatar.com/avatar/)
|
||||
*
|
||||
* All other URLs (including arbitrary http(s):// origins) are rejected to
|
||||
* prevent client-side IP/UA/Referer leaks to attacker-controlled servers.
|
||||
*/
|
||||
export function safeImageUrl(url: string): string {
|
||||
if (!url || url === '') {
|
||||
return `${WEBUI_BASE_URL}${PLACEHOLDER_IMAGE}`;
|
||||
}
|
||||
|
||||
if (
|
||||
url.startsWith(WEBUI_BASE_URL) ||
|
||||
url.startsWith('https://www.gravatar.com/avatar/') ||
|
||||
url.startsWith('data:') ||
|
||||
url.startsWith('/')
|
||||
) {
|
||||
return url;
|
||||
}
|
||||
|
||||
return `${WEBUI_BASE_URL}${PLACEHOLDER_IMAGE}`;
|
||||
}
|
||||
Reference in New Issue
Block a user