How to check which third parties receive data
List every request whose origin differs from the page, then read what each one carried: query string, POST body, Referer and Cookie. On the fixture below five requests went to one third party. Three carried nothing but the origin. Two carried an email address, a page path and a screen size.
Why check this
A list of third-party hosts is not a list of data recipients. A stylesheet, a logo and an analytics call all appear as requests to the same outside origin, and only one of them describes the person reading the page. A report that counts hosts says a font vendor and a data broker are the same finding, and the team stops reading it.
Run this before sign-off on any page that shows account data, and again after a tag manager change. The failure it catches is an address bar parameter, an email in this fixture, reaching an outside origin through the Referer header on requests nobody thinks of as tracking, including the stylesheet.
Prerequisites
- Node 22. Start both files below in separate shells. The page is served from
localhost:9622and the second origin is127.0.0.1:9623, which is a different site, so these are real cross-site requests. - Chrome 152 driven over the DevTools Protocol, because the Network panel does not show the
Refereron every row at a glance. - The figures are one capture on one machine on 2026-09-12.
// tp.mjs the second origin. It logs what it received and serves three harmless files.
import { createServer } from 'node:http';
const files = {
'/widget.css': ['text/css', 'body{font-family:system-ui}'],
'/logo.png': ['image/png', ''],
'/t.js': ['application/javascript', `
fetch('http://127.0.0.1:9623/collect', {
method: 'POST', headers: { 'content-type': 'text/plain' },
body: JSON.stringify({ email: new URL(location.href).searchParams.get('email'),
path: location.pathname, screen: screen.width + 'x' + screen.height })
});
new Image().src = 'http://127.0.0.1:9623/px.gif?ev=view&uid=u-9f31';`],
};
createServer((req, res) => {
const body = [];
req.on('data', (c) => body.push(c));
req.on('end', () => {
console.log(JSON.stringify({
method: req.method, url: req.url,
referer: req.headers.referer ?? null,
body: Buffer.concat(body).toString() || null,
}));
const path = req.url.split('?')[0];
if (files[path]) {
res.writeHead(200, { 'content-type': files[path][0], 'cache-control': 'no-store' });
res.end(files[path][1]);
} else {
res.writeHead(204, { 'cache-control': 'no-store' });
res.end();
}
});
}).listen(9623, '127.0.0.1', () => console.log('third party listening on 127.0.0.1:9623'));
// shop.mjs the page under test. /leaky serves the same page with a wider referrer policy.
import { createServer } from 'node:http';
const T = 'http://127.0.0.1:9623';
const page = (leaky) => `<!doctype html><meta charset="utf-8"><title>Order 4417</title>
${leaky ? '<meta name="referrer" content="unsafe-url">' : ''}
<link rel="stylesheet" href="${T}/widget.css">
<h1>Order 4417</h1><img src="${T}/logo.png" alt="partner">
<script src="${T}/t.js"></script>`;
createServer((req, res) => {
res.writeHead(200, { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store' });
res.end(page(req.url.startsWith('/leaky')));
}).listen(9622, 'localhost', () => console.log('shop listening on localhost:9622'));
// carried.mjs lists every cross-origin request and what it carried
import { open } from '../../scripts/browser/session.mjs';
const target = process.argv[2];
const s = await open();
try {
const reqs = new Map();
await s.cdp.send('Network.enable');
s.cdp.on('Network.requestWillBeSent', (e) =>
reqs.set(e.requestId, { url: e.request.url, type: e.type, method: e.request.method,
post: e.request.postData ?? null, headers: e.request.headers }));
await s.goto(target);
await new Promise((r) => setTimeout(r, 700));
const pageOrigin = new URL(target).origin;
console.log('page origin: ' + pageOrigin);
console.log('');
for (const r of reqs.values()) {
const u = new URL(r.url);
if (u.origin === pageOrigin) continue;
const carried = [];
const q = [...u.searchParams];
if (q.length) carried.push('query ' + q.map(([k, v]) => `${k}=${v}`).join('&'));
if (r.post) carried.push('body ' + r.post);
const ref = r.headers.Referer ?? r.headers.referer;
if (ref) carried.push('referer ' + ref);
const ck = r.headers.Cookie ?? r.headers.cookie;
if (ck) carried.push('cookie ' + ck);
console.log(`${r.method} ${u.origin}${u.pathname} [${r.type}]`);
console.log(carried.length ? carried.map((c) => ' ' + c).join('\n') : ' (nothing but the request itself)');
}
} finally { await s.close(); }
// conscheck.mjs what the browser reported about the same load
import { open } from '../../scripts/browser/session.mjs';
const s = await open();
try {
await s.goto('http://localhost:9622/account/orders/4417?email=ada%40example.org');
await new Promise((r) => setTimeout(r, 700));
for (const m of s.console) console.log(m);
for (const f of s.failed) console.log('failed: ' + f.url + ' ' + f.reason);
} finally { await s.close(); }
Steps
- Step 1.
Load the page with an identifier in the address bar and list what each outside request carried.
node carried.mjs "http://localhost:9622/account/orders/4417?email=ada%40example.org"page origin: http://localhost:9622 GET http://127.0.0.1:9623/widget.css [Stylesheet] referer http://localhost:9622/ GET http://127.0.0.1:9623/logo.png [Image] referer http://localhost:9622/ GET http://127.0.0.1:9623/t.js [Script] referer http://localhost:9622/ POST http://127.0.0.1:9623/collect [Fetch] body {"email":"ada@example.org","path":"/account/orders/4417","screen":"800x600"} referer http://localhost:9622/ GET http://127.0.0.1:9623/px.gif [Image] query ev=view&uid=u-9f31 referer http://localhost:9622/Five requests, one outside origin, two findings. The stylesheet, the logo and the script carry only
http://localhost:9622/, the origin of the page. ThePOST /collectcarries the email address and the screen size, andGET /px.gifcarries a user id. Those two are the data transfers. The other three are dependencies. - Step 2.
Confirm the finding on the receiving side, where nothing can be argued away.
cat tp.logthird party listening on 127.0.0.1:9623 {"method":"GET","url":"/widget.css","referer":"http://localhost:9622/","body":null} {"method":"GET","url":"/t.js","referer":"http://localhost:9622/","body":null} {"method":"GET","url":"/logo.png","referer":"http://localhost:9622/","body":null} {"method":"POST","url":"/collect","referer":"http://localhost:9622/","body":"{\"email\":\"ada@example.org\",\"path\":\"/account/orders/4417\",\"screen\":\"800x600\"}"} {"method":"GET","url":"/px.gif?ev=view&uid=u-9f31","referer":"http://localhost:9622/","body":null}The server recorded the same five calls and the same body. The order of the three static files differs from the browser's list, and it differs again between runs, because they finish in whatever order the sockets complete.
- Step 3.
Read what the browser reported about that same load.
node conscheck.mjserror: Access to fetch at 'http://127.0.0.1:9623/collect' from origin 'http://localhost:9622' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. error: Failed to load resource: net::ERR_FAILED pageerror: Uncaught (in promise) TypeError: Failed to fetchThe
POST /collectis a failed request. The console calls it blocked, the request list marks itnet::ERR_FAILED, and the page threw. Step 2 shows the same request arriving at the third party with the email address in its body. CORS withheld the response from the page. It did not stop the data leaving. - Step 4.
Load the variant whose only difference is a referrer policy, and compare the same list.
node carried.mjs "http://localhost:9622/leaky/account/orders/4417?email=ada%40example.org"page origin: http://localhost:9622 GET http://127.0.0.1:9623/widget.css [Stylesheet] referer http://localhost:9622/leaky/account/orders/4417?email=ada%40example.org GET http://127.0.0.1:9623/logo.png [Image] referer http://localhost:9622/leaky/account/orders/4417?email=ada%40example.org GET http://127.0.0.1:9623/t.js [Script] referer http://localhost:9622/leaky/account/orders/4417?email=ada%40example.org POST http://127.0.0.1:9623/collect [Fetch] body {"email":"ada@example.org","path":"/leaky/account/orders/4417","screen":"800x600"} referer http://localhost:9622/leaky/account/orders/4417?email=ada%40example.org GET http://127.0.0.1:9623/px.gif [Image] query ev=view&uid=u-9f31 referer http://localhost:9622/leaky/account/orders/4417?email=ada%40example.orgOne
<meta name="referrer" content="unsafe-url">moved the stylesheet and the logo from the second column to the first. The email address now reaches the outside origin five times instead of once, and two of those requests are for files with no script in them. - Step 5.
Count the recipients rather than the requests, which is the number that belongs in a report.
node carried.mjs "http://localhost:9622/account/orders/4417?email=ada%40example.org" | grep -c "^GET\|^POST"5Five cross-origin requests and one recipient origin. Report the origin, then the two requests that carried a value, and keep the other three out of the finding.
How to read the result
| What you see | What it means | What to do |
| --- | --- | --- |
| Only a referer line, showing an origin | A dependency, not a data transfer | Record the vendor, close the finding. |
| A referer line showing a full path and query | Your URL is the payload | Set a Referrer-Policy and stop putting identifiers in URLs. |
| A query or body line with a stable id | An identifier was handed over | Confirm it is covered by consent and by the contract with that vendor. |
| A cookie line on a cross-origin request | The vendor recognises this browser from earlier visits | Check whether the cookie survives third-party restriction. |
| A recipient that appears only after a click | A tag fired on interaction | Repeat the capture with the interaction included. |
Common mistakes
What to check next
- How to check for tracking pixels: the smallest shape a data transfer takes, and the beacon that outlives the page.
- How to check referer header: the policy that decides how much of your URL every outside request carries.
- How to check for pii in urls: removes the payload at the source, so the referrer question stops mattering.
- How to check which third party scripts a page loads: the code that decides what gets sent at runtime.
- How to check browser fingerprinting: data collected from the browser rather than from your page.
FAQ
How to check website trackers without an extension?
Drive the page over the DevTools Protocol, as in step 1, or read the Network panel row by row with the Headers panel open. An extension classifies by host list. This classifies by what each request carried, which is the question being asked.
How to see trackers on website pages behind a login?
Run the same capture on a signed-in session. Account pages are where identifiers appear in URLs, so they carry more in the Referer than a marketing page does, and the analytics call has more to describe.
How to check for trackers that only fire on a click?
A page-load capture misses them. Keep the driver running, perform the interaction, then print the list. Every request is timestamped by the protocol, so a request that appears only after the click is easy to attribute.
How to detect websites use trackers when the requests go to a first-party subdomain?
Compare hosts, not just registrable domains, and read the response. A subdomain that answers with a 204 and no body is an event endpoint whoever operates it. Where the data goes next is a server question, not a browser one.
How to identify websites that use trackers across a whole site?
Run the capture on one page per template, not on every URL. The tag set changes by template, and repeating a home page capture a thousand times produces the same five rows.
Verified
Verified by Maks Vernynode 22.23.2Chrome 152.0.7977.76
Each output block is what the command above it printed on that date, on the host named in the step. Figures read from a live site move between runs. Compare the shape of the answer rather than the digits, and see the methodology for how a page is re-verified.
Related on this site
intermediate15 minpublished updated Maks Verny