GHSA-226F-F24G-524W

Vulnerability from github – Published: 2026-06-17 14:10 – Updated: 2026-06-17 14:10
VLAI
Summary
Open WebUI: Redirect-Bypass SSRF in OAuth `_process_picture_url` (incomplete-fix sibling of CVE-2026-45401)
Details

Summary

backend/open_webui/utils/oauth.py::_process_picture_url (v0.9.5, lines 1435-1470) calls validate_url(picture_url) on the initial URL only, then invokes aiohttp.ClientSession.get(picture_url, ...) without allow_redirects=False. aiohttp's default is allow_redirects=True, max_redirects=10; the function does not pass the project's AIOHTTP_CLIENT_ALLOW_REDIRECTS env constant either. An attacker with a valid OAuth IdP identity can therefore submit a public URL that 302-redirects to an internal address and read the internal response body via the attacker's own profile_image_url field.

This is the same redirect-bypass class as CVE-2026-45401 (GHSA-rh5x-h6pp-cjj6), on a 6th call site that the v0.9.5 patch missed. CVE-2026-45401's advisory body enumerates exactly five affected paths — SafeWebBaseLoader._scrape, _fetch, get_content_from_url, load_url_image, get_image_base64_from_url — none in utils/oauth.py.

Vulnerable code (v0.9.5)

backend/open_webui/utils/oauth.py, lines 1435-1470:

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)                              # initial URL only

        get_kwargs = {}
        if access_token:
            get_kwargs['headers'] = {'Authorization': f'Bearer {access_token}'}
        async with aiohttp.ClientSession(trust_env=True) as session:
            async with session.get(picture_url, **get_kwargs,
                                   ssl=AIOHTTP_CLIENT_SESSION_SSL) as resp:
            #                       ^^^^^^^^^^^ no allow_redirects=False
                if resp.ok:
                    picture = await resp.read()
                    base64_encoded_picture = base64.b64encode(picture).decode('utf-8')
                    guessed_mime_type = mimetypes.guess_type(picture_url)[0]
                    if guessed_mime_type is None:
                        guessed_mime_type = 'image/jpeg'
                    return f'data:{guessed_mime_type};base64,{base64_encoded_picture}'
                ...

The function is invoked at oauth.py:1556 (new-user OAuth signup) and oauth.py:1536 (existing-user picture update on login). Neither call site re-validates after redirect-following.

backend/open_webui/retrieval/web/utils.py (v0.9.5) imports the env constant AIOHTTP_CLIENT_ALLOW_REDIRECTS at line 51 and uses it on the five paths patched by CVE-2026-45401. utils/oauth.py does not import or reference it.

Exploitation

Preconditions: - ENABLE_OAUTH_SIGNUP=true or OAUTH_UPDATE_PICTURE_ON_LOGIN=true (common in production OAuth-IdP deployments) - Attacker has a valid identity on the configured OAuth IdP (Google, Microsoft, GitHub, or any generic OIDC provider)

Steps:

  1. Attacker hosts a redirect endpoint at http://attacker.example/r on a public IP. validate_url("http://attacker.example/r") returns True (is_global=True for public IPs).
  2. Attacker sets their IdP picture claim to http://attacker.example/r.
  3. Attacker signs in to open-webui via OAuth. open-webui invokes _process_picture_url("http://attacker.example/r", ...).
  4. validate_url accepts the public URL. session.get("http://attacker.example/r") is invoked.
  5. attacker.example responds HTTP/1.1 302 Found\r\nLocation: http://127.0.0.1:11434/api/tags. (Or http://169.254.169.254/latest/meta-data/iam/security-credentials/, RFC1918 internal services, etc.)
  6. aiohttp follows the redirect server-side. No re-validation.
  7. The internal response body is read into picture, base64-encoded, and stored as profile_image_url = "data:image/jpeg;base64,..." on the attacker's account.
  8. Attacker reads back via GET /api/v1/auths/. Decode the base64 payload to get the full internal response body.

Impact

Full-read SSRF, identical read-back primitive to CVE-2026-45338:

  • Cloud metadata services (AWS IMDSv1 at 169.254.169.254, GCP metadata.google.internal, Azure IMDS) → IAM credentials, managed-identity tokens
  • Localhost-bound services (Ollama at :11434, Redis, Elasticsearch, internal Postgres exporters)
  • RFC1918 internal infrastructure not exposed to the internet

Distinction from prior CVEs

Prior CVE This finding Distinguishing fact
CVE-2026-45338 (GHSA-24c9) _process_picture_url had no validate_url() call at all Fixed in v0.9.0 by adding the call. Ours is the call being insufficient because it doesn't loop over redirect targets. Different mechanism, different fix.
CVE-2026-45400 (GHSA-8w7q) validate_url() had urlparse-vs-requests parser disagreement on \@ chars Fixed in v0.9.5 by char-blocklist. Ours is post-validation redirect-following — orthogonal mechanism.
CVE-2026-45401 (GHSA-rh5x) Five paths in retrieval, routers/images, utils/files, utils/middleware Parent class. Same CWE-918 redirect-bypass mechanism. utils/oauth.py::_process_picture_url is not among the five paths in the parent advisory's "Affected code paths" section. Same class, missed sink. Direct sibling.

Suggested fix

async with session.get(
    picture_url,
    **get_kwargs,
    ssl=AIOHTTP_CLIENT_SESSION_SSL,
    allow_redirects=AIOHTTP_CLIENT_ALLOW_REDIRECTS,   # add
) as resp:

Or, if redirects must remain enabled by default, wrap in a manual-follow loop that re-invokes validate_url() on each Location header. This mirrors the fix shape applied to the five paths in CVE-2026-45401.

Affected versions

Vulnerable: <= 0.9.5 Fix: 0.9.6

References

  • CVE-2026-45401 / GHSA-rh5x-h6pp-cjj6 (parent cluster, redirect-bypass on 5 paths)
  • CVE-2026-45338 / GHSA-24c9-2m8q-qhmh (original _process_picture_url SSRF, patched v0.9.0)
  • CVE-2026-45400 / GHSA-8w7q-q5jp-jvgx (validate_url parser-disagreement bypass, patched v0.9.5)
  • open-webui issue #24560 (corroborates that the v0.9.5 redirect-fix was applied piecemeal across call sites)

Proof of Concept

End-to-end PoC executed against ghcr.io/open-webui/open-webui:v0.9.5 in Docker compose. Three services: attacker (OIDC IdP + 302-redirect endpoint on evil.example.com:9001/redirect), canary (internal target on internal-target.local:9002/sentinel), open-webui v0.9.5.

Fresh-CSPRNG sentinel generated after OAuth state-establishing call (per Gate 5.5 oracle protocol): SSRF-POC-5580111b2a0d7d0c8324bfa92a0d9d09.

Result: - profile_image_url field after OAuth login: data:image/jpeg;base64,U1NSRi1QT0MtNTU4MDExMWIyYTBkN2QwYzgzMjRiZmE5MmEwZDlkMDk= - Base64 decode: SSRF-POC-5580111b2a0d7d0c8324bfa92a0d9d09 (byte-for-byte sentinel match) - Canary log: !!! SSRF HIT - sentinel served

Chain confirmed: OAuth login → IdP returns picture claim evil.example.com:9001/redirect → validate_url() accepts FQDN → aiohttp.ClientSession.get(...) follows 302 to internal-target.local:9002/sentinel server-side without re-validation → response body base64-encoded into attacker's profile_image_url → readable via GET /api/v1/auths/.

PoC artifacts (compose, attacker server, canary, run/verify scripts, full transcript) available on request.

Reporter

Matteo Panzeri — GitHub: matte1782, contact: matteo1782@gmail.com. Requesting CVE credit as Matteo Panzeri.

Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 0.9.5"
      },
      "package": {
        "ecosystem": "PyPI",
        "name": "open-webui"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "0.9.6"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-54008"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-918"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-06-17T14:10:56Z",
    "nvd_published_at": null,
    "severity": "HIGH"
  },
  "details": "## Summary\n\n`backend/open_webui/utils/oauth.py::_process_picture_url` (v0.9.5, lines 1435-1470) calls `validate_url(picture_url)` on the initial URL only, then invokes `aiohttp.ClientSession.get(picture_url, ...)` without `allow_redirects=False`. aiohttp\u0027s default is `allow_redirects=True, max_redirects=10`; the function does not pass the project\u0027s `AIOHTTP_CLIENT_ALLOW_REDIRECTS` env constant either. An attacker with a valid OAuth IdP identity can therefore submit a public URL that 302-redirects to an internal address and read the internal response body via the attacker\u0027s own `profile_image_url` field.\n\nThis is the same redirect-bypass class as CVE-2026-45401 (GHSA-rh5x-h6pp-cjj6), on a 6th call site that the v0.9.5 patch missed. CVE-2026-45401\u0027s advisory body enumerates exactly five affected paths \u00e2\u20ac\u201d `SafeWebBaseLoader._scrape`, `_fetch`, `get_content_from_url`, `load_url_image`, `get_image_base64_from_url` \u00e2\u20ac\u201d none in `utils/oauth.py`.\n\n## Vulnerable code (v0.9.5)\n\n`backend/open_webui/utils/oauth.py`, lines 1435-1470:\n\n```python\nasync def _process_picture_url(self, picture_url: str, access_token: str = None) -\u003e str:\n    if not picture_url:\n        return \u0027/user.png\u0027\n    try:\n        validate_url(picture_url)                              # initial URL only\n\n        get_kwargs = {}\n        if access_token:\n            get_kwargs[\u0027headers\u0027] = {\u0027Authorization\u0027: f\u0027Bearer {access_token}\u0027}\n        async with aiohttp.ClientSession(trust_env=True) as session:\n            async with session.get(picture_url, **get_kwargs,\n                                   ssl=AIOHTTP_CLIENT_SESSION_SSL) as resp:\n            #                       ^^^^^^^^^^^ no allow_redirects=False\n                if resp.ok:\n                    picture = await resp.read()\n                    base64_encoded_picture = base64.b64encode(picture).decode(\u0027utf-8\u0027)\n                    guessed_mime_type = mimetypes.guess_type(picture_url)[0]\n                    if guessed_mime_type is None:\n                        guessed_mime_type = \u0027image/jpeg\u0027\n                    return f\u0027data:{guessed_mime_type};base64,{base64_encoded_picture}\u0027\n                ...\n```\n\nThe function is invoked at `oauth.py:1556` (new-user OAuth signup) and `oauth.py:1536` (existing-user picture update on login). Neither call site re-validates after redirect-following.\n\n`backend/open_webui/retrieval/web/utils.py` (v0.9.5) imports the env constant `AIOHTTP_CLIENT_ALLOW_REDIRECTS` at line 51 and uses it on the five paths patched by CVE-2026-45401. `utils/oauth.py` does not import or reference it.\n\n## Exploitation\n\n**Preconditions:**\n- `ENABLE_OAUTH_SIGNUP=true` or `OAUTH_UPDATE_PICTURE_ON_LOGIN=true` (common in production OAuth-IdP deployments)\n- Attacker has a valid identity on the configured OAuth IdP (Google, Microsoft, GitHub, or any generic OIDC provider)\n\n**Steps:**\n\n1. Attacker hosts a redirect endpoint at `http://attacker.example/r` on a public IP. `validate_url(\"http://attacker.example/r\")` returns True (`is_global=True` for public IPs).\n2. Attacker sets their IdP `picture` claim to `http://attacker.example/r`.\n3. Attacker signs in to open-webui via OAuth. open-webui invokes `_process_picture_url(\"http://attacker.example/r\", ...)`.\n4. `validate_url` accepts the public URL. `session.get(\"http://attacker.example/r\")` is invoked.\n5. attacker.example responds `HTTP/1.1 302 Found\\r\\nLocation: http://127.0.0.1:11434/api/tags`. (Or `http://169.254.169.254/latest/meta-data/iam/security-credentials/`, RFC1918 internal services, etc.)\n6. aiohttp follows the redirect server-side. **No re-validation.**\n7. The internal response body is read into `picture`, base64-encoded, and stored as `profile_image_url = \"data:image/jpeg;base64,...\"` on the attacker\u0027s account.\n8. Attacker reads back via `GET /api/v1/auths/`. Decode the base64 payload to get the full internal response body.\n\n## Impact\n\nFull-read SSRF, identical read-back primitive to CVE-2026-45338:\n\n- Cloud metadata services (AWS IMDSv1 at `169.254.169.254`, GCP `metadata.google.internal`, Azure IMDS) \u00e2\u2020\u2019 IAM credentials, managed-identity tokens\n- Localhost-bound services (Ollama at `:11434`, Redis, Elasticsearch, internal Postgres exporters)\n- RFC1918 internal infrastructure not exposed to the internet\n\n## Distinction from prior CVEs\n\n| Prior CVE | This finding | Distinguishing fact |\n|---|---|---|\n| CVE-2026-45338 (GHSA-24c9) | `_process_picture_url` had no `validate_url()` call at all | Fixed in v0.9.0 by adding the call. Ours is the call being insufficient because it doesn\u0027t loop over redirect targets. Different mechanism, different fix. |\n| CVE-2026-45400 (GHSA-8w7q) | `validate_url()` had urlparse-vs-requests parser disagreement on `\\@` chars | Fixed in v0.9.5 by char-blocklist. Ours is post-validation redirect-following \u00e2\u20ac\u201d orthogonal mechanism. |\n| CVE-2026-45401 (GHSA-rh5x) | Five paths in retrieval, routers/images, utils/files, utils/middleware | Parent class. Same CWE-918 redirect-bypass mechanism. `utils/oauth.py::_process_picture_url` is not among the five paths in the parent advisory\u0027s \"Affected code paths\" section. Same class, missed sink. Direct sibling. |\n\n## Suggested fix\n\n```python\nasync with session.get(\n    picture_url,\n    **get_kwargs,\n    ssl=AIOHTTP_CLIENT_SESSION_SSL,\n    allow_redirects=AIOHTTP_CLIENT_ALLOW_REDIRECTS,   # add\n) as resp:\n```\n\nOr, if redirects must remain enabled by default, wrap in a manual-follow loop that re-invokes `validate_url()` on each `Location` header. This mirrors the fix shape applied to the five paths in CVE-2026-45401.\n\n## Affected versions\n\nVulnerable: `\u003c= 0.9.5`\nFix: 0.9.6\n\n## References\n\n- CVE-2026-45401 / GHSA-rh5x-h6pp-cjj6 (parent cluster, redirect-bypass on 5 paths)\n- CVE-2026-45338 / GHSA-24c9-2m8q-qhmh (original `_process_picture_url` SSRF, patched v0.9.0)\n- CVE-2026-45400 / GHSA-8w7q-q5jp-jvgx (`validate_url` parser-disagreement bypass, patched v0.9.5)\n- open-webui issue #24560 (corroborates that the v0.9.5 redirect-fix was applied piecemeal across call sites)\n\n## Proof of Concept\n\nEnd-to-end PoC executed against `ghcr.io/open-webui/open-webui:v0.9.5` in Docker compose. Three services: attacker (OIDC IdP + 302-redirect endpoint on `evil.example.com:9001/redirect`), canary (internal target on `internal-target.local:9002/sentinel`), open-webui v0.9.5.\n\nFresh-CSPRNG sentinel generated **after** OAuth state-establishing call (per Gate 5.5 oracle protocol): `SSRF-POC-5580111b2a0d7d0c8324bfa92a0d9d09`.\n\nResult:\n- `profile_image_url` field after OAuth login: `data:image/jpeg;base64,U1NSRi1QT0MtNTU4MDExMWIyYTBkN2QwYzgzMjRiZmE5MmEwZDlkMDk=`\n- Base64 decode: `SSRF-POC-5580111b2a0d7d0c8324bfa92a0d9d09` (byte-for-byte sentinel match)\n- Canary log: `!!! SSRF HIT - sentinel served`\n\nChain confirmed: OAuth login \u00e2\u2020\u2019 IdP returns picture claim `evil.example.com:9001/redirect` \u00e2\u2020\u2019 `validate_url()` accepts FQDN \u00e2\u2020\u2019 `aiohttp.ClientSession.get(...)` follows 302 to `internal-target.local:9002/sentinel` server-side without re-validation \u00e2\u2020\u2019 response body base64-encoded into attacker\u0027s `profile_image_url` \u00e2\u2020\u2019 readable via `GET /api/v1/auths/`.\n\nPoC artifacts (compose, attacker server, canary, run/verify scripts, full transcript) available on request.\n\n## Reporter\n\nMatteo Panzeri \u00e2\u20ac\u201d GitHub: `matte1782`, contact: `matteo1782@gmail.com`. Requesting CVE credit as **Matteo Panzeri**.",
  "id": "GHSA-226f-f24g-524w",
  "modified": "2026-06-17T14:10:56Z",
  "published": "2026-06-17T14:10:56Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/open-webui/open-webui/security/advisories/GHSA-226f-f24g-524w"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/open-webui/open-webui"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:L/A:N",
      "type": "CVSS_V3"
    }
  ],
  "summary": "Open WebUI: Redirect-Bypass SSRF in OAuth `_process_picture_url` (incomplete-fix sibling of CVE-2026-45401)"
}


Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.

Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…