VYPR
Medium severity6.5GHSA Advisory· Published May 15, 2026

CVE-2026-45667

CVE-2026-45667

Description

Open WebUI is a self-hosted artificial intelligence platform designed to operate entirely offline. Prior to 0.8.0, GET /api/v1/memories/ef is accessible without authentication and executes request.app.state.EMBEDDING_FUNCTION(...). This allows any unauthenticated caller to trigger embedding generation which can lead to direct cost exposure if a paid provider is used. This vulnerability is fixed in 0.8.0.

Affected products

1

Patches

1
e5035ea31e17

refac

https://github.com/open-webui/open-webuiTimothy Jaeryang BaekFeb 11, 2026via ghsa
6 files changed · +99 25
  • backend/open_webui/routers/memories.py+1 6 modified
    @@ -18,11 +18,6 @@
     router = APIRouter()
     
     
    -@router.get("/ef")
    -async def get_embeddings(request: Request):
    -    return {"result": await request.app.state.EMBEDDING_FUNCTION("hello world")}
    -
    -
     ############################
     # GetMemories
     ############################
    @@ -165,7 +160,7 @@ async def reset_memory_from_vector_db(
         user=Depends(get_verified_user),
     ):
         """Reset user's memory vector embeddings.
    -    
    +
         CRITICAL: We intentionally do NOT use Depends(get_session) here.
         This endpoint generates embeddings for ALL user memories in parallel using
         asyncio.gather(). A user with 100 memories would trigger 100 embedding API
    
  • backend/open_webui/utils/middleware.py+3 1 modified
    @@ -2099,9 +2099,10 @@ async def process_chat_payload(request, form_data, user, metadata, model):
     
         # Skills: inject manifest only — model uses view_skill tool to load full content on-demand
         user_skill_ids = form_data.pop("skill_ids", None) or []
    -    model_skill_ids = model.get("info", {}).get("meta", {}).get("skills", [])
    +    model_skill_ids = model.get("info", {}).get("meta", {}).get("skillIds", [])
     
         all_skill_ids = list(set(user_skill_ids + model_skill_ids))
    +    available_skills = []
         if all_skill_ids:
             from open_webui.models.skills import Skills as SkillsModel
     
    @@ -2348,6 +2349,7 @@ async def tool_function(**kwargs):
                 {
                     **extra_params,
                     "__event_emitter__": event_emitter,
    +                "__skill_ids__": [s.id for s in available_skills],
                 },
                 features,
                 model,
    
  • backend/open_webui/utils/tools.py+1 1 modified
    @@ -508,7 +508,7 @@ def is_builtin_tool_enabled(category: str) -> bool:
             )
     
         # Skills tools - view_skill allows model to load full skill instructions on demand
    -    if is_builtin_tool_enabled("skills"):
    +    if extra_params.get("__skill_ids__"):
             builtin_functions.append(view_skill)
     
         for func in builtin_functions:
    
  • src/lib/components/workspace/Models/ModelEditor.svelte+15 0 modified
    @@ -12,6 +12,7 @@
     	import Tags from '$lib/components/common/Tags.svelte';
     	import Knowledge from '$lib/components/workspace/Models/Knowledge.svelte';
     	import ToolsSelector from '$lib/components/workspace/Models/ToolsSelector.svelte';
    +	import SkillsSelector from '$lib/components/workspace/Models/SkillsSelector.svelte';
     	import FiltersSelector from '$lib/components/workspace/Models/FiltersSelector.svelte';
     	import ActionsSelector from '$lib/components/workspace/Models/ActionsSelector.svelte';
     	import Capabilities from '$lib/components/workspace/Models/Capabilities.svelte';
    @@ -89,6 +90,7 @@
     
     	let knowledge = [];
     	let toolIds = [];
    +	let skillIds = [];
     
     	let filterIds = [];
     	let defaultFilterIds = [];
    @@ -166,6 +168,14 @@
     			}
     		}
     
    +		if (skillIds.length > 0) {
    +			info.meta.skillIds = skillIds;
    +		} else {
    +			if (info.meta.skillIds) {
    +				delete info.meta.skillIds;
    +			}
    +		}
    +
     		if (filterIds.length > 0) {
     			info.meta.filterIds = filterIds;
     		} else {
    @@ -293,6 +303,7 @@
     			});
     
     			toolIds = model?.meta?.toolIds ?? [];
    +			skillIds = model?.meta?.skillIds ?? [];
     			filterIds = model?.meta?.filterIds ?? [];
     			defaultFilterIds = model?.meta?.defaultFilterIds ?? [];
     			actionIds = model?.meta?.actionIds ?? [];
    @@ -736,6 +747,10 @@
     						<ToolsSelector bind:selectedToolIds={toolIds} tools={$tools ?? []} />
     					</div>
     
    +					<div class="my-4">
    +						<SkillsSelector bind:selectedSkillIds={skillIds} />
    +					</div>
    +
     					{#if ($functions ?? []).filter((func) => func.type === 'filter').length > 0 || ($functions ?? []).filter((func) => func.type === 'action').length > 0}
     						<hr class=" border-gray-100/30 dark:border-gray-850/30 my-4" />
     
    
  • src/lib/components/workspace/Models/SkillsSelector.svelte+62 0 added
    @@ -0,0 +1,62 @@
    +<script lang="ts">
    +	import Checkbox from '$lib/components/common/Checkbox.svelte';
    +	import Tooltip from '$lib/components/common/Tooltip.svelte';
    +	import { getContext, onMount } from 'svelte';
    +
    +	import { getSkillItems } from '$lib/apis/skills';
    +
    +	export let selectedSkillIds: string[] = [];
    +
    +	let _skills: Record<string, any> = {};
    +
    +	const i18n = getContext('i18n');
    +
    +	onMount(async () => {
    +		const res = await getSkillItems(localStorage.token).catch(() => null);
    +		const skills = res?.items ?? [];
    +		_skills = skills.reduce((acc: Record<string, any>, skill: any) => {
    +			acc[skill.id] = {
    +				...skill,
    +				selected: selectedSkillIds.includes(skill.id)
    +			};
    +
    +			return acc;
    +		}, {});
    +	});
    +</script>
    +
    +<div>
    +	<div class="flex w-full justify-between mb-1">
    +		<div class=" self-center text-xs font-medium text-gray-500">{$i18n.t('Skills')}</div>
    +	</div>
    +
    +	<div class="flex flex-col mb-1">
    +		{#if Object.keys(_skills).length > 0}
    +			<div class=" flex items-center flex-wrap">
    +				{#each Object.keys(_skills) as skill, skillIdx}
    +					<div class=" flex items-center gap-2 mr-3">
    +						<div class="self-center flex items-center">
    +							<Checkbox
    +								state={_skills[skill].selected ? 'checked' : 'unchecked'}
    +								on:change={(e) => {
    +									_skills[skill].selected = e.detail === 'checked';
    +									selectedSkillIds = Object.keys(_skills).filter((s) => _skills[s].selected);
    +								}}
    +							/>
    +						</div>
    +
    +						<Tooltip content={_skills[skill]?.description ?? _skills[skill].id}>
    +							<div class=" py-0.5 text-sm w-full capitalize font-medium">
    +								{_skills[skill].name}
    +							</div>
    +						</Tooltip>
    +					</div>
    +				{/each}
    +			</div>
    +		{/if}
    +	</div>
    +
    +	<div class=" text-xs dark:text-gray-700">
    +		{$i18n.t('To select skills here, add them to the "Skills" workspace first.')}
    +	</div>
    +</div>
    
  • src/lib/components/workspace/Skills.svelte+17 17 modified
    @@ -54,22 +54,7 @@
     	let viewOption = '';
     	let page = 1;
     
    -	// Debounce only query changes
    -	$: if (query !== undefined) {
    -		loading = true;
    -		clearTimeout(searchDebounceTimer);
    -		searchDebounceTimer = setTimeout(() => {
    -			page = 1;
    -			getSkillItems();
    -		}, 300);
    -	}
    -
    -	// Immediate response to page/filter changes
    -	$: if (page && viewOption !== undefined) {
    -		getSkillItems();
    -	}
    -
    -	const getSkillItems = async () => {
    +	const loadSkillItems = async () => {
     		if (!loaded) return;
     
     		loading = true;
    @@ -95,6 +80,21 @@
     		}
     	};
     
    +	// Debounce only query changes
    +	$: if (query !== undefined) {
    +		loading = true;
    +		clearTimeout(searchDebounceTimer);
    +		searchDebounceTimer = setTimeout(() => {
    +			page = 1;
    +			loadSkillItems();
    +		}, 300);
    +	}
    +
    +	// Immediate response to page/filter changes
    +	$: if (page && viewOption !== undefined) {
    +		loadSkillItems();
    +	}
    +
     	const cloneHandler = async (skill) => {
     		const _skill = await getSkillById(localStorage.token, skill.id).catch((error) => {
     			toast.error(`${error}`);
    @@ -136,7 +136,7 @@
     		}
     
     		page = 1;
    -		getSkillItems();
    +		loadSkillItems();
     		await _skills.set(await getSkills(localStorage.token));
     	};
     
    

Vulnerability mechanics

AI mechanics synthesis has not run for this CVE yet.

References

4

News mentions

0

No linked articles in our index yet.