How to check cookies on a website
Load the page, then read the profile's cookie store with CDP Network.getAllCookies instead of document.cookie. The jar returns name, domain, path, expiry, HttpOnly and SameSite for every cookie. On the fixture below the jar held six cookies where document.cookie showed four, missing both HttpOnly ones.
Why check this
A cookie inventory is the input to every other privacy check. The consent banner test, the retention review and the privacy notice all need the same list: which cookies exist, who set them, how long they live. Build it once per release, and again after any change to the tag manager or a third-party script.
The failure it prevents is an inventory that is quietly short. document.cookie is the obvious instrument and it cannot see HttpOnly cookies, by design. On the fixture below that gap hides two names, and one of them is the server-set tracking identifier, the cookie the inventory exists to find. A list built that way looks complete and omits the item that matters.
The second gap is the opposite shape. A cookie a third party tried to set and the browser refused never enters the jar at all, so no jar read will ever mention it. That fact lives in the network events, and a reader who only inspects storage will report a clean result for a request that was trying to track.
Prerequisites
- Node 22 and the Chrome already installed. Nothing is downloaded except the driver.
npm i puppeteer-core. The jar read is Network.getAllCookies, which returns the whole cookie store for the profile, not only the cookies the current document can read.- A fresh profile for each run, so the list describes this page load rather than the machine's browsing history.
- The two servers below.
consent-fixture.mjson port 9601 is the site under test.pixel-origin.mjson port 9602 is a separate origin, so the page has a real cross-site request. Both are local, so no host outside the project is touched. - One capture on one machine, Chrome 152 on Windows, 2026-09-12. Cookie expiry dates move with the run date.
// consent-fixture.mjs node consent-fixture.mjs -> http://localhost:9601/
import { createServer } from 'node:http';
const YEAR = 'Max-Age=31536000; Path=/';
const records = new Map(); // sid -> consent record
const html = `<!doctype html><meta charset="utf-8"><title>Fixture shop</title>
<h1>Fixture shop</h1><p id="state">consent: not recorded</p>
<div id="banner"><p>We use cookies for analytics and marketing.</p>
<button id="accept">Accept all</button> <button id="reject">Reject all</button></div>
<button id="withdraw">Withdraw consent</button>
<script>
const send = (d) => fetch('/consent', { method: 'POST', headers: { 'content-type': 'application/json' },
body: JSON.stringify({ decision: d }) }).then((r) => r.json()).then((j) => {
state.textContent = 'consent: ' + j.decision;
banner.hidden = j.decision !== 'withdraw';
localStorage.setItem('fx_consent', JSON.stringify({ decision: j.decision, at: j.at }));
if (j.decision === 'accept') localStorage.setItem('fx_analytics_queue', '["pageview:/"]');
sessionStorage.setItem('fx_banner_seen', '1');
});
accept.onclick = () => send('accept');
reject.onclick = () => send('reject');
withdraw.onclick = () => send('withdraw');
</script>`;
createServer((req, res) => {
const sid = /fx_sid=([^;]+)/.exec(req.headers.cookie || '')?.[1] || 'sid-7f3a';
if (req.url === '/echo') {
res.writeHead(200, { 'content-type': 'application/json' });
return res.end(JSON.stringify({ cookieHeader: req.headers.cookie || null }));
}
if (req.url === '/consent/record') {
res.writeHead(200, { 'content-type': 'application/json' });
return res.end(JSON.stringify(records.get(sid) || null, null, 2));
}
if (req.url === '/consent' && req.method === 'POST') {
let body = '';
req.on('data', (c) => (body += c));
return req.on('end', () => {
const decision = JSON.parse(body).decision;
const at = new Date().toISOString();
const prev = records.get(sid);
records.set(sid, decision === 'withdraw'
? { ...prev, decision: 'withdraw', withdrawnAt: at }
: { id: 'cr-' + Math.random().toString(36).slice(2, 10), subject: sid, decision, at,
categories: { necessary: true, analytics: decision === 'accept', marketing: decision === 'accept' },
policyVersion: '2026-03-01', bannerVersion: '1.4.0', scope: 'http://localhost:9601',
method: 'banner-button' });
// The defect under test: both decisions set the same analytics cookies.
const jar = decision === 'withdraw' ? [] : [`fx_ga=GA1.2.884.1789; ${YEAR}`, `fx_fbp=fb.1.771; ${YEAR}`,
`fx_uid=u-91af; HttpOnly; ${YEAR}`];
res.writeHead(200, { 'content-type': 'application/json',
'set-cookie': [...jar, `fx_consent=${decision}; ${YEAR}`] });
res.end(JSON.stringify({ decision, at }));
});
}
res.writeHead(200, { 'content-type': 'text/html; charset=utf-8',
'set-cookie': [`fx_sid=${sid}; HttpOnly; Path=/`, `fx_seen=1; ${YEAR}`] });
res.end(html + (req.url.includes('tp=1') ? '<img src="http://127.0.0.1:9602/px.gif" alt="">' : ''));
}).listen(9601, () => console.log('fixture on http://localhost:9601/'));
// pixel-origin.mjs node pixel-origin.mjs -> http://127.0.0.1:9602/px.gif
import { createServer } from 'node:http';
const gif = Buffer.from('R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7', 'base64');
createServer((req, res) => {
res.writeHead(200, { 'content-type': 'image/gif',
'set-cookie': ['tp_id=t-55; Max-Age=31536000; Path=/; SameSite=None'] });
res.end(gif);
}).listen(9602, () => console.log('pixel origin on http://127.0.0.1:9602/'));
// cookie-inventory.mjs node cookie-inventory.mjs
import { launch } from 'puppeteer-core';
import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
const profile = mkdtempSync(join(tmpdir(), 'consent-'));
const browser = await launch({ executablePath: 'C:/Program Files/Google/Chrome/Application/chrome.exe',
headless: true, userDataDir: profile });
try {
const page = (await browser.pages())[0];
const cdp = await page.createCDPSession();
await cdp.send('Network.enable');
await page.goto('http://localhost:9601/', { waitUntil: 'networkidle2' });
await page.click('#accept');
await page.waitForFunction("state.textContent !== 'consent: not recorded'");
const js = await page.evaluate(() => document.cookie);
console.log('document.cookie :', js);
const { cookies } = await cdp.send('Network.getAllCookies');
console.log('\nname domain path expires httpOnly sameSite');
for (const c of cookies.sort((a, b) => a.name.localeCompare(b.name))) {
console.log([c.name.padEnd(10), c.domain.padEnd(11), c.path.padEnd(4),
(c.expires === -1 ? 'session' : new Date(c.expires * 1000).toISOString().slice(0, 10)).padEnd(10),
String(c.httpOnly).padEnd(8), c.sameSite || '(unset)'].join(' '));
}
const jsNames = new Set(js.split('; ').map((c) => c.split('=')[0]));
console.log('\njar count', cookies.length, ' document.cookie count', jsNames.size);
console.log('in the jar, not in document.cookie :',
cookies.filter((c) => !jsNames.has(c.name)).map((c) => c.name).join(' '));
} finally {
await browser.close();
rmSync(profile, { recursive: true, force: true });
}
// cookie-thirdparty.mjs node cookie-thirdparty.mjs
import { launch } from 'puppeteer-core';
import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
const profile = mkdtempSync(join(tmpdir(), 'consent-'));
const browser = await launch({ executablePath: 'C:/Program Files/Google/Chrome/Application/chrome.exe',
headless: true, userDataDir: profile });
try {
const page = (await browser.pages())[0];
const cdp = await page.createCDPSession();
await cdp.send('Network.enable');
const refused = [];
cdp.on('Network.responseReceivedExtraInfo', (e) => (e.blockedCookies || []).forEach(
(b) => refused.push(`${b.cookieLine.split(';')[0]} ${b.blockedReasons.join(',')}`)));
await page.goto('http://localhost:9601/?tp=1', { waitUntil: 'networkidle2' });
const { cookies } = await cdp.send('Network.getAllCookies');
console.log('domains in the jar :', [...new Set(cookies.map((c) => c.domain))].join(' '));
console.log('cookies on 127.0.0.1 :', cookies.filter((c) => c.domain === '127.0.0.1').length);
console.log('Set-Cookie refused :', refused.join(' | ') || 'none');
} finally {
await browser.close();
rmSync(profile, { recursive: true, force: true });
}
Steps
- Step 1.
Start the site under test.
node consent-fixture.mjsfixture on http://localhost:9601/ - Step 2.
Start the second origin in another shell, so the page has a cross-site request to inspect.
node pixel-origin.mjspixel origin on http://127.0.0.1:9602/ - Step 3.
Take the inventory: what script can read, then what the browser is holding.
node cookie-inventory.mjsdocument.cookie : fx_seen=1; fx_ga=GA1.2.884.1789; fx_fbp=fb.1.771; fx_consent=accept name domain path expires httpOnly sameSite fx_consent localhost / 2027-09-12 false (unset) fx_fbp localhost / 2027-09-12 false (unset) fx_ga localhost / 2027-09-12 false (unset) fx_seen localhost / 2027-09-12 false (unset) fx_sid localhost / session true (unset) fx_uid localhost / 2027-09-12 true (unset) jar count 6 document.cookie count 4 in the jar, not in document.cookie : fx_sid fx_uidRead the table column by column.
expiresofsessionmeans the cookie dies with the browser, sofx_sidis the only one here that does not persist. The four dated cookies all land on 2027-09-12, one year out, which is theMax-Agethe server sent.httpOnlyis true for exactly the two names the last line reports as missing fromdocument.cookie, andfx_uidis a tracking identifier.sameSiteis unset on all six, so Chrome treats them asLax. - Step 4.
Load the variant that pulls an image from the other origin, and read what the browser refused.
node cookie-thirdparty.mjsdomains in the jar : localhost cookies on 127.0.0.1 : 0 Set-Cookie refused : tp_id=t-55 SameSiteNoneInsecureThe jar holds one domain and no third-party cookie, which on its own reads as a clean result. The third line says a cookie was offered and rejected, and names the reason:
SameSite=NonewithoutSecureis refused outright. The attempt is in the inventory only because the network event was read.
How to read the result
| What you see | What it means | What to do |
| --- | --- | --- |
| jar count above document.cookie count | HttpOnly cookies exist | Keep the CDP list as the inventory. Note each missing name and its owner. |
| expires is session | The cookie ends with the browser | Check it is a session identifier and not analytics wearing a short lifetime. |
| A date far in the future | A persistent cookie | Compare it to the retention the privacy notice states. See How to check cookie expiry. |
| domain differs from the page host | The cookie belongs to another party | Name the owner and the purpose before deciding which consent category it falls under. |
| Set-Cookie refused is not none | Something tried to set a cookie and failed | Read the reason. A refusal today is a cookie that returns as soon as the flag is fixed. |
| sameSite is (unset) | The server sent no attribute | Chrome applies Lax. Record the effective value, not the blank. |
Common mistakes
What to check next
- How to check if cookies are secure and HttpOnly: the two flags in the table, and what each one protects against.
- How to check cookie expiry: turning the
expirescolumn into a retention answer. - How to check SameSite cookie attribute: what the
(unset)in this capture means in practice. - How to check if third party cookies are blocked: the refusal in step 4, read from the browser's own settings.
- How to test cookie consent banner: running this inventory twice, once per banner button, and diffing the two.
FAQ
How do I see website cookies without DevTools open?
Drive Chrome from a script and call Network.getAllCookies over the DevTools Protocol, as in step 3. The browser needs to run; the panel does not. How to check cookie flags with curl covers the part that needs no browser at all.
How do I do a cookie audit?
Run the inventory once per consent path and once per route that loads different scripts, then merge the lists. For each name record the setter, the purpose, the lifetime and whether it is first or third party.
Why does the browser show more cookies than my script?
document.cookie hides HttpOnly cookies from script, which is the point of the flag. The DevTools panel and Network.getAllCookies read the store directly and list them.
Can I scan a website for cookies from the command line?
The command line shows Set-Cookie headers on the responses you request. Cookies written by page script, and cookies set by subresources the browser fetched on its own, need a browser to appear.
Do blocked cookies belong in the inventory?
Yes, as attempts. Record the name, the origin and the block reason. A cookie refused for a missing Secure flag becomes a live cookie the day someone adds it.
Verified
Verified by Maks Vernynode 22.23.2Chrome 152.0.7977.76puppeteer-core 25.10.0
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
basic8 minpublished updated Maks Verny