Catching Cloudflare WARP Leaking Real IPs Through Tor | AI-Native security platform
Cloudflare WARP Gateway leaks real IP addresses through Tor via undocumented HTTP headers.
Summary
A security researcher discovered that Cloudflare's managed WARP Gateway service injects HTTP headers (including Cf-Connecting-Ip and the undocumented Cf-Warp-Tag-Id) before traffic enters Tor, exposing users' real IP addresses. The Cf-Warp-Tag-Id header persisted across two sessions linking a Finnish VPS to a French residential connection two hours apart, enabling IP correlation. This is an architectural mismatch where HTTP-layer inspection stamps identifying metadata upstream of TCP-layer anonymization, defeating Tor's anonymity guarantees.
Full text
HoneypotCatching in the WildNetwork Analysis Catching Cloudflare WARP Leaking Real IPs Through Tor A visitor reached a Beelzebub honeypot emulating Ollama through six Tor exit nodes across four countries. Every request carried five Cloudflare headers, including the visitor's real IP. The undocumented header Cf-Warp-Tag-Id persisted across two sessions from two different source IPs, linking an anonymous VPS to a residential broadband connection two hours apart. Zachary Gardner Cybersecurity Researcher Table of Contents TL;DR A visitor reached a Beelzebub honeypot emulating Ollama through six Tor exit nodes across four countries. Every request carried five Cloudflare headers, including the visitor’s real IP. The undocumented header Cf-Warp-Tag-Id persisted across two sessions from two different source IPs, linking an anonymous VPS to a residential broadband connection two hours apart. Cloudflare’s managed WARP Gateway injects attribution headers at the HTTP layer before traffic enters Tor. Tor transports TCP and does not inspect HTTP content. The headers survive intact. This is an architectural mismatch, not a Tor vulnerability. Any HTTP-level proxy that stamps identifying headers upstream of a TCP-layer anonymizer will leak them through it. Prior art: SANS ISC Diary 32532 (Ullrich, Dec 2025). This post extends that work with cross-IP correlation, Tor survival analysis, and evidence from a non-Cloudflare destination. Background Cloudflare WARP ships in two tiers. The consumer tier (1.1.1.1 app) encrypts DNS traffic to Cloudflare’s edge. The managed tier (Zero Trust / Gateway) adds HTTP-level inspection, TLS termination, and policy enforcement. In December 2025, Johannes Ullrich at SANS ISC observed Cf-Warp-Tag-Id in honeypot scanning traffic and framed it as CDN bypass (https://isc.sans.edu/diary/32532). A 2023 test (https://github.com/szepeviktor/cloudflare-warp-http-headers) concluded WARP “adds no extra headers,” but that test used the consumer app. Managed WARP through Gateway behaves differently. Cloudflare does not document the distinction, and Cf-Warp-Tag-Id does not appear in their official headers reference (https://developers.cloudflare.com/fundamentals/reference/http-headers/). The Honeypot Setup The sensor runs Beelzebub with protocol: "http" emulating Ollama on port 11434. Static handlers serve /api/tags and /api/version. The LLMHoneypot plugin handles /api/generate and /api/chat with dynamic responses. Decision Rationale DecisionRationalePort 11434Default Ollama binding. Scanners expect it.Static /api/tagsPasses scanner validation without a running model.LLM plugin on inference endpointsDynamic responses sustain engagement past initial probes. Beelzebub’s HTTP handler logs the full request header map in the HeadersMap field of every trace event. Non-standard headers, including Cloudflare’s, are captured automatically. No additional configuration required. apiVersion: "v1" protocol: "http" address: ":11434" description: "Ollama LLM inference server" commands: - regex: "^/api/tags$" handler: | {"models":[{"name":"llama3:latest","model":"llama3:latest", "modified_at":"2024-12-15T10:30:00Z","size":4661211808, "digest":"sha256:abc123","details":{"format":"gguf", "family":"llama","parameter_size":"8.0B", "quantization_level":"Q4_0"}}]} headers: - "Content-Type: application/json" statusCode: 200 - regex: "^/api/version$" handler: '{"version":"0.5.4"}' headers: - "Content-Type: application/json" statusCode: 200 - regex: "^/$" handler: "Ollama is running" statusCode: 200 - regex: "^/api/(generate|chat)$" plugin: "LLMHoneypot" headers: - "Content-Type: application/json" statusCode: 200 The port has no Cloudflare infrastructure in the request path. No Tunnel, no DNS proxying, no reverse proxy. Timeline of Events Phase 1 — Direct Reconnaissance (March 21) 65.109.30[.]32 (Hetzner, Helsinki) connected directly. Client: python-requests/2.32.5. Queried /api/tags, received a model list, sent test prompts, disconnected. No Cloudflare headers present. This session established a baseline for the IP. Phase 2 — Anonymized Requests (March 31, 17:27 - 17:46 UTC) Ten days later, traffic arrived from six Tor exit nodes across Sweden, the Netherlands, Germany, and Switzerland. Every request carried:Cf-Connecting-Ip: 65.109.30[.]32 Cf-Warp-Tag-Id: b87b4e0d-XXXX-XXXX-XXXX-5c7413e6d372 Cf-Ipcountry: FI Cf-Ray: 9e5111121b0fe7bc-FRA Cdn-Loop: cloudflare; loops=1Cf-Connecting-Ip matched the VPS from Phase 1. The visitor had added WARP and Tor to their path. Cloudflare’s Gateway recorded the origin IP before the traffic entered Tor.A custom client (xdash-client/1.0, no public footprint) sent 19 requests to /api/embed over 18 minutes containing job-related content. Phase 3 — Residential Session (March 31, 19:34 UTC) Two hours later, a session arrived from residential IPv6 on Free SAS (AS12322, French ISP). Chrome 146, Linux, Accept-Language: fr-FR.Cf-Connecting-Ip: 2a01:e0a:fe2:4de0:fac:5473:377e[:]1cde Cf-Warp-Tag-Id: b87b4e0d-XXXX-XXXX-XXXX-5c7413e6d372 Cf-Ipcountry: FR Cf-Ray: 9e51cabb2dd36ffa-CDGEverything changed except Cf-Warp-Tag-Id. One header linked a Finnish VPS to a French residential connection through six Tor exits.The visitor hit the LLM-backed /api/chat endpoint and spent nine prompts extracting credentials and enumerating a database the honeypot improvised in real time. Phase 4 — Return (April 1, 17:40 UTC) One request from the Finnish VPS via xdash-client/1.0. Same Cf-Warp-Tag-Id. No further activity. What the Honeypot Captured Embedding Requests (Phase 2) The xdash-client/1.0 sessions sent POST requests to /api/embed with model: nomic-embed-text. The request bodies contained real-world content, not test strings: A 4,093-character software developer resume, sent multiple times. The resume contains verifiable PII (name, GitHub profile, education, employment history). OSINT confirms the individual exists. The resume dates to approximately 2019–2020 based on star counts and missing recent roles. This is scraped job board data, not synthetic test content. { "model": "nomic-embed-text", "input": "[REDACTED] Senior Software Engineer... San Jose State University... Experience: [REDACTED large tech companies]... Open source: [REDACTED project name] (12K+ stars)... Skills: React, Node.js, TypeScript, Python, AWS, Docker, Kubernetes..." } Subsequent requests contained job posting search queries and full position listings: "SAP ABAP developer with Fiori and CDS experience" "SAP ABAP developer with 5 years experience" Two Australian job listings followed (Melbourne-based companies, one requiring NV1 security clearance). Between these, a debug string: "test embedding provider path". The content pattern is consistent with building a vector-similarity job matching system on an endpoint the visitor does not own. Chat Session (Phase 3) The residential session hit /api/chat. The honeypot’s LLM-backed response handler served a command menu, then planted credentials, then improvised a database schema across nine prompts. The full captured sequence: Visitor: help Server: Available commands: GET /path, POST /path {json}, env, dbquery "SQL", creds Visitor: creds Server: AWS_ACCESS_KEY_ID=AKIA[REDACTED] AWS_SECRET_ACCESS_KEY=[REDACTED] DATABASE_URL=postgresql://[REDACTED]@db-primary. [REDACTED]:5432/[REDACTED] VAULT_TOKEN=hvs.[REDACTED] Visitor: env Server: [Same credentials repeated as environment variables] Visitor: SELECT table_name FROM information_schema.tables WHERE table_schema = 'public'; Server: ["users", "orders", "shipments", "inventory", "invoices"] Visitor: select * from orders Server: [5 rows with order_id, user_id, amount, status, created_at] Visitor: SELECT * FROM orders Server: [Same result, case-corrected query] Visitor: SELECT column_name FROM information_schema.columns WHERE table_name = 'orders'; Server: [order_id, user_id, product_id, quantity, amount, status, created_at] Visitor: SELECT column_name FROM information_schema.columns WHERE table_name = 'users
Indicators of Compromise
- ip — 65.109.30.32
- ip — 2a01:e0a:fe2:4de0:fac:5473:377e:1cde