OSV 1.4.0 · github-reviewed · 修改于 2026-07-21 05:03
发布时间
2026-06-17 22:11
GitHub 审查时间
2026-06-17 22:11
NVD 发布时间
2026-06-24 02:18
源文件
advisories/github-reviewed/2026/06/GHSA-wch8-mhj5-9frg/GHSA-wch8-mhj5-9frg.json
POST /api/chat/completions accepts an image_url.url value that, when it does NOT start with http://, https://, or data:image/, is interpreted as a file id and resolved against the global file table with no ownership check. An authenticated user can therefore set image_url.url to another user's file id, the server reads that file from disk, base64-encodes it, and injects the data URI into the LLM request. The user then prompts the LLM to describe / OCR the file and reads the content back.
Same class as CVE-2026-44560 (RAG cross-user access) and the multiple has_access_to_file checks added in routers/files.py -- the auth boundary was tightened on the file router but not on this conversion path.
backend/open_webui/utils/middleware.py:2113-2150 -- convert_url_images_to_base64:
async def convert_url_images_to_base64(form_data):
messages = form_data.get('messages', [])
for message in messages:
content = message.get('content')
if not isinstance(content, list):
continue
new_content = []
for item in content:
if not isinstance(item, dict) or item.get('type') != 'image_url':
new_content.append(item)
continue
image_url = item.get('image_url', {}).get('url', '')
if image_url.startswith('data:image/'):
new_content.append(item)
continue
try:
base64_data = await get_image_base64_from_url(image_url) # <-- no `user` passed
if base64_data:
new_content.append({'type': 'image_url',
'image_url': {'url': base64_data}})
called from the main chat completion middleware at middleware.py:2357:
form_data = await convert_url_images_to_base64(form_data)
backend/open_webui/utils/files.py:57-95 -- get_image_base64_from_url:
async def get_image_base64_from_url(url: str) -> Optional[str]:
try:
if url.startswith('http'):
validate_url(url)
# ... SSRF-safe fetch with allow_redirects=AIOHTTP_CLIENT_ALLOW_REDIRECTS ...
else:
file = await Files.get_file_by_id(url) # <-- NO user_id filter
if not file:
return None
file_path = await asyncio.to_thread(Storage.get_file, file.path)
file_path = Path(file_path)
if file_path.is_file():
with open(file_path, 'rb') as image_file:
encoded_string = base64.b64encode(image_file.read()).decode('utf-8')
content_type = mimetypes.guess_type(file_path.name)[0] or (file.meta or {}).get('content_type')
...
return f'data:{content_type};base64,{encoded_string}'
Files.get_file_by_id in models/files.py:161 does a bare db.get(File, id) -- no ownership filter. there is a separate Files.get_file_by_id_and_user_id at line 172 that does filter on user_id, and the file router uses has_access_to_file(id, 'read', user, db) at routers/files.py:626 etc. neither check exists on this path.
c7f1d8e3-..../api/v1/chat/completions with body:{
"model": "<any vision model>",
"messages": [
{
"role": "user",
"content": [
{"type": "text", "text": "transcribe everything you can see in this image"},
{"type": "image_url", "image_url": {"url": "c7f1d8e3-..."}}
]
}
]
}
Server reads user A's file from disk, base64-encodes it, and sends to the LLM as user B's image attachment. LLM response contains the file content.
File ids are UUIDs and not enumerable directly, but they leak via:
/api/v1/chats/{id}) include file idsAny authenticated user can read any other user's file content (image and any file with an image-guess mimetype path) via this channel. Severity is bounded by what the LLM will accept in image_url -- in practice, image files work cleanly with any vision model; pdf / docx work with multi-modal providers that accept them.
Thread the authenticated user through to get_image_base64_from_url and resolve the file via Files.get_file_by_id_and_user_id(id, user.id) (or has_access_to_file(id, 'read', user, db) if shared-via-knowledge-base access is intended). Same pattern that's already used in routers/files.py:626 and elsewhere.
minimal patch sketch:
--- a/backend/open_webui/utils/files.py
+++ b/backend/open_webui/utils/files.py
@@ -57,7 +57,7 @@
-async def get_image_base64_from_url(url: str) -> Optional[str]:
+async def get_image_base64_from_url(url: str, user=None) -> Optional[str]:
try:
if url.startswith('http'):
...
else:
- file = await Files.get_file_by_id(url)
+ file = (await Files.get_file_by_id_and_user_id(url, user.id)
+ if user is not None else None)
+ if file is None:
+ # fall back to access-grant check for shared files
+ file = await Files.get_file_by_id(url)
+ if file and not await has_access_to_file(url, 'read', user):
+ return None
and pipe user through convert_url_images_to_base64(form_data, user) from the middleware caller. happy to send a PR once you confirm the fix shape you want.
this was found via patch-diffing existing advisories. the same bug class likely exists in any other site that calls Files.get_file_by_id without an adjacent has_access_to_file / get_file_by_id_and_user_id check. quick grep:
git grep -n 'Files\.get_file_by_id(' -- 'backend/open_webui/**'
worth a sweep across utils/ and routers/ for missed sites.
Open-webui main branch as of commit 3660bc0 (2026-05-10). python 3.x backend. confirmed by reading the source; no instance stood up.