VulnWatch VulnWatch
← Back to dashboard
Medium github · GHSA-wch8-mhj5-9frg

Open WebUI: Cross-user file disclosure via /api/chat/completions image_url field

Published Jun 17, 2026 CVSS 6.5

summary

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.

affected code

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)  #  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)        #  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.

variant note

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.

environment

Open-webui main branch as of commit 3660bc0 (2026-05-10). python 3.x backend. confirmed by reading the source; no instance stood up.

Affected AI Products

llm
Get the weekly digest. Every Monday: top AI security stories of the week. Free.