ISP Header Injection Explained: Supercookies, X-UIDH, and How to Detect Them
Some ISPs have injected hidden tracking identifiers into every HTTP request you make โ invisible to you, readable by advertisers. Here's how it works, when HTTPS stops it, what can still happen, and how to verify it yourself.
- โSome ISPs have historically injected tracking headers (e.g. Verizon's
X-UIDH) into HTTP requests, allowing advertisers to identify users even after clearing cookies. - โModern HTTPS encrypts headers end-to-end, so injection requires a TLS-terminating middlebox and a trusted certificate.
- โCorporate networks, captive portals, and some ISPs still operate such middleboxes.
- โDetection method: a server-side echo endpoint strips infrastructure headers and returns the remainder. Anything unexpected was injected in transit.
- โFalse positives exist: corporate proxies, VPNs, CDNs, and captive portals all add headers that look like injection.
Real-World Cases of ISP Header Injection
It has happened at scale, documented by regulators and researchers.
The Network Path: Where Injection Happens
To understand header injection you need to understand the network path of an HTTP request. There are several points between your browser and a destination server where a middlebox can read and modify the request.
[Your Browser]
GET / HTTP/1.1
Host: example.com
User-Agent: Chrome/121
|
| (your home network)
|
[ISP Router / Edge Node]
|
| โโโโโโโโโโโโโโโโโโโโ
| โ ISP middlebox adds: โ
| โ X-UIDH: A1B2C3D4 โ โ you never see this
| โโโโโโโโโโโโโโโโโโโโ
|
[Destination Server]
receives:
GET / HTTP/1.1
Host: example.com
User-Agent: Chrome/121
X-UIDH: A1B2C3D4 โ injected headerThe browser sent 3 headers. The server received 4. The browser has no way to observe this. It never sees what headers the server actually received.
Why HTTPS Mostly Prevents This
When you visit an HTTPS site, the connection is encrypted using TLS. The ISP can see the destination hostname (via SNI in the TLS handshake) but cannot read or modify the request headers or body; they're encrypted inside the TLS tunnel.
This is why Verizon's UIDH injection and most historical cases targeted HTTP traffic. As HTTPS adoption grew (it reached ~95% of browser traffic by 2023), mass header injection became largely infeasible.
There are exceptions though:
- TLS-terminating middleboxes: A proxy that has a certificate your device trusts can terminate TLS, inspect and modify the decrypted request, then re-encrypt it toward the destination. The browser shows a padlock. The connection was intercepted. Corporate DLP appliances, some hotel networks, and documented ISP deployments do this.
- Remaining HTTP traffic: Not all traffic is HTTPS. APIs, IoT devices, and legacy applications still make unencrypted requests.
- CONNECT proxy tunnels: HTTP CONNECT proxies can read the plaintext before the TLS layer, depending on how the proxy is configured.
The Detection Technique: Server-Side Echo
The cleanest way to see what your ISP is adding is to run a server-side echo: send a request from the browser, have a server capture the exact headers it received, and send them back. The diff between what the browser sent and what the server received is what your ISP injected.
This is exactly what our test does. The /api/echo endpoint is a Next.js route deployed on Vercel. When your browser hits it, the server captures all incoming headers, strips known infrastructure headers (Vercel adds around 15 of its own), and returns the remainder as JSON. Your browser then checks that JSON against a list of known ISP tracking headers.
// Strip all Vercel/CDN infrastructure headers
const STRIP_PREFIXES = ['x-vercel-', 'x-amz-', 'cf-'];
const STRIP_EXACT = new Set([
'host', 'accept', 'accept-encoding', 'accept-language',
'x-forwarded-for', 'x-forwarded-proto', 'forwarded', ...
]);
// Whatever remains is what your ISP added
const filtered = {};
for (const [key, value] of request.headers) {
if (!shouldStrip(key)) filtered[key] = value;
}
return Response.json({ receivedHeaders: filtered });The tricky part is the strip list. Vercel adds headers like x-vercel-ip-country, x-vercel-ja4-digest, x-matched-path, and around a dozen others. If you don't strip these, every user would appear to have "proxy headers", which is exactly the false positive that triggered complaints at launch. The fix is prefix-based stripping rather than enumerating every header name, since Vercel adds new ones regularly.
Header Tampering: Did a Proxy Rewrite Your Request?
Header injection (ISP adds new headers) is one problem. Header modification is another: a proxy in the path rewrites headers your browser sent. This is a strong indicator of a man-in-the-middle device: a corporate DPI appliance, a transparent proxy, or ISP-operated traffic management equipment.
We detect this by capturing User-Agent and Accept-Language server-side and comparing them to navigator.userAgent and navigator.languages on the client. If the base language tag changed (e.g. en-US became en) or the User-Agent string was normalised, a proxy rewrote it. This is a genuine real-world signal: Blue Coat proxies, Squid, and some enterprise DLP tools are known to modify these headers.
WebRTC and the IP Exposure Problem
WebRTC is the browser API that powers video calls, peer-to-peer file sharing, and similar features. It works by discovering your public IP address via STUN (Session Traversal Utilities for NAT) servers (a standard protocol for punching through firewalls).
The problem: any website can initiate a WebRTC peer connection and collect your IP address as part of the ICE candidate discovery process. This happens over a side channel, not through a normal HTTP request, so it bypasses proxies and in many configurations it bypasses VPN tunnels too.
For a regular user on a home ISP this is mostly informational; the IP is already visible via HTTP. For VPN users it is a genuine leak: the VPN hides the connection IP, but WebRTC can reveal the real ISP-assigned IP to any website, defeating the anonymity the VPN was supposed to provide.
const pc = new RTCPeerConnection({
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'stun:stun.cloudflare.com:3478' },
],
});
pc.createDataChannel('');
pc.onicecandidate = (event) => {
const ip = /(d{1,3}.){3}d{1,3}/.exec(
event.candidate?.candidate ?? ''
)?.[0];
// Filter out RFC-1918 private ranges (192.168.x, 10.x, 172.16-31.x)
// Whatever remains is a public IP
};
await pc.setLocalDescription(await pc.createOffer());To mitigate: in Firefox, set media.peerconnection.enabled to false in about:config. In Chrome or Edge, uBlock Origin has a "Prevent WebRTC from leaking local IP addresses" option. Most VPN clients also have a WebRTC leak prevention setting.
What We Can't Detect (and Why)
Being honest about limitations matters. Here is what this type of browser-based test genuinely cannot do:
- DNS resolver identificationA browser cannot query which DNS server handled a given lookup. The only reliable way to test is with a dedicated DNS server you control: make a unique lookup, check if it arrived at your server, and from which resolver. We can't do this in a browser. We tell you if your connection IP belongs to a known VPN or privacy network, but that's not the same as knowing your DNS resolver.
- HTTPS traffic inspectionIf your ISP is performing TLS inspection, the connection is terminated and re-encrypted before it reaches our server. We use indirect signals (Cloudflare WARP detection, Cloudflare trace anomalies) but these are not definitive. A sophisticated middlebox with a trusted CA certificate would be very hard to detect without client-side certificate pinning.
- Deep packet inspectionDPI at the ISP level happens below the TCP layer in some cases. Traffic shaping and protocol classification can be done without injecting headers โ there is no way to detect this from a browser.
- Historical dataWe can only test what is happening right now on this connection. Your ISP's behaviour may vary by network segment, time of day, or traffic type.
False Positives: When It's Not Your ISP
This is important. Our tool (and any header echo tool) will show headers that look suspicious but are completely benign. Here are the main sources of false positives:
How to Verify This Yourself
You don't need our tool. You can run the same check with curl. Point it at any server you control (or use a public echo service) and inspect the headers it received:
# See exactly what headers a server receives from you curl -v https://httpbin.org/headers 2>&1 | grep "^>" # Or use a verbose request to see both sent and received curl -v https://httpbin.org/headers # httpbin /headers returns a JSON of every request header # it received โ compare to what your browser shows in # DevTools โ Network โ any request โ Request Headers
You can also check in browser DevTools: open Network tab, make any request, click it, and look at "Request Headers". These are the headers your browser sent. Compare that to what a server echo endpoint returns โ any difference was added in transit.
For a more thorough check, run the same curl request from your home network and from a mobile hotspot. Different injection behaviour on different networks is a strong signal.
The score is a weighted sum starting at 100. Each check applies a penalty or bonus:
| Signal | Score change |
|---|---|
| Known tracking header found (UIDH, X-ACT, etc.) | โ35 |
| Header modified in transit (proxy tampering) | โ25 |
| No VPN / WARP / self-attested DoH | โ20 |
| VPN/WARP IP detected, or user confirmed DoH | +8 to +9 |
| TLS interception signal | โ40 |
| WebRTC bypass while on VPN | โ15 |
| ISP with documented surveillance history (AT&T, Verizon) | โ10 |
| ISP with documented practices (Comcast, Spectrum, Cox) | โ8 |
| Clean echo โ no suspicious headers | +2 |
Scores are clamped to 0โ100. A score of 75+ is Low risk, 45โ74 is Medium, below 45 is High.
Try It
The full test runs in your browser in about 5 seconds. No sign-up, no install. Your IP is hashed with a daily-rotating salt before storage โ we cannot reverse it.