How to check global privacy control

Global Privacy Control is one bit sent two ways: the Sec-GPC: 1 request header and the navigator.globalPrivacyControl property. Point the browser under test at a server that echoes both, then compare them. Brave 1.95 reported true and sent the header. Chrome 152 sent neither.

Why check this

Run this before a release that touches opt-out handling, and again in regression whenever the consent layer changes. The signal is not decoration: the California Attorney General's CCPA FAQ says of GPC that "Under law, it must be honored by covered businesses as a valid consumer request to stop the sale or sharing of personal information."

The failure this catches is a one-sided implementation. Server code reads Sec-GPC and the front end reads navigator.globalPrivacyControl, and nobody checks that a real browser sets both. A visitor whose extension injects only the header then opts out on the wire while the page script sees nothing, so the opt-out branch never runs and the request is logged as consent.

Prerequisites

import { createServer } from 'node:http';
const WATCH = ['sec-gpc', 'dnt', 'user-agent'];
createServer((req, res) => {
  const seen = WATCH.map((h) => `${h}: ${req.headers[h] ?? '(absent)'}`).join('\n');
  console.log(`${req.method} ${req.url}\n${seen}\n`);
  res.setHeader('content-type', 'text/html; charset=utf-8');
  res.end(`<!doctype html><title>echo</title><pre>${seen}</pre>`);
}).listen(9641, '127.0.0.1', () => console.log('echo listening on http://127.0.0.1:9641/'));
import { launch } from 'puppeteer-core';
const BIN = {
  chrome: 'C:/Program Files/Google/Chrome/Application/chrome.exe',
  brave: 'C:/Program Files/BraveSoftware/Brave-Browser/Application/brave.exe',
};
const which = process.argv[2];
const browser = await launch({ executablePath: BIN[which], headless: true });
const page = await browser.newPage();
if (process.argv.includes('--inject')) {
  const cdp = await page.createCDPSession();
  await cdp.send('Network.enable');
  await cdp.send('Network.setExtraHTTPHeaders', { headers: { 'Sec-GPC': '1' } });
}
await page.goto('http://127.0.0.1:9641/');
const r = await page.evaluate(() => ({
  value: navigator.globalPrivacyControl,
  present: 'globalPrivacyControl' in navigator,
  echoed: document.querySelector('pre').textContent,
}));
console.log(`${which} ${await browser.version()}`);
console.log(`navigator.globalPrivacyControl: ${r.value} (in navigator: ${r.present})`);
console.log(`server saw:\n${r.echoed}`);
await browser.close();

Steps

  1. Step 1.

    Send the header by hand, so the server side can be tested without a browser at all.

    curl -s -H 'Sec-GPC: 1' http://127.0.0.1:9641/
    
    <!doctype html><title>echo</title><pre>sec-gpc: 1
    dnt: (absent)
    user-agent: curl/8.21.0</pre>

    One header, one value. Any other value is outside the specification.

  2. Step 2.

    Read what the default browser does. Nothing is enabled and no extension is loaded.

    node probe.mjs chrome
    
    chrome Chrome/152.0.7977.76
    navigator.globalPrivacyControl: undefined (in navigator: false)
    server saw:
    sec-gpc: (absent)
    dnt: (absent)
    user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/152.0.0.0 Safari/537.36

    in navigator: false is the line that matters. The property is not defined and set to nothing, it is absent from the object.

  3. Step 3.

    Read a browser that ships the signal switched on.

    node probe.mjs brave
    
    brave Chrome/153.0.8010.37
    navigator.globalPrivacyControl: true (in navigator: true)
    server saw:
    sec-gpc: 1
    dnt: (absent)
    user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/153.0.0.0 Safari/537.36

    Both forms agree, which is what the specification describes: the property "enables a client-side script to determine what Sec-GPC header field value was sent".

  4. Step 4.

    Inject the header into Chrome the way a proxy or an extension does, and read the property again.

    node probe.mjs chrome --inject
    
    chrome Chrome/152.0.7977.76
    navigator.globalPrivacyControl: undefined (in navigator: false)
    server saw:
    sec-gpc: 1
    dnt: (absent)
    user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/152.0.0.0 Safari/537.36

    The two forms now disagree. The server sees an opt-out and the page script sees nothing.

How to read the result

| What you see | What it means | What to do | | --- | --- | --- | | Header 1 and property true | The browser sends the signal natively | Test both code paths against this browser. | | Header absent, property absent | The browser has no GPC support | A missing property is not the same as an opt-out declined. | | Header 1, property undefined | Something on the network path added the header | Read the header on the server, not the property in the page. | | Header absent, property true | A script or extension set the property only | Server code will never see the opt-out. Treat as a defect. |

Common mistakes

Sign: An extension or proxy is set to send GPC, yet navigator.globalPrivacyControl stays undefined.Cause: A header added on the network path does not create the DOM property. In the capture above, CDP injection put Sec-GPC: 1 on the wire while the page still reported in navigator: false. Front-end opt-out logic keyed to the property misses every such visitor.
Sign: A test asserts navigator.globalPrivacyControl === false and fails in Chrome.Cause: Chrome 152 does not define the property at all, so the value is undefined, not false. Read 'globalPrivacyControl' in navigator first, then the value, and keep 'unknown' as a third state.
Sign: Server code treats any Sec-GPC header as an opt-out.Cause: The specification defines exactly one field value, 1. A truthiness test in JavaScript accepts the string '0' as well, which inverts the user's choice. Compare the value to '1'.

What to check next

FAQ

What is a GPC signal?

One bit that says the visitor opts out of the sale or sharing of their personal data. It travels as the request header Sec-GPC: 1 and, in browsers that support it natively, as the boolean navigator.globalPrivacyControl.

How do I enable global privacy control?

Use a browser that sends it. Brave sent it with no configuration in the capture above. Chrome 152 has no setting for it, so testers add an extension or a proxy, which produces the header without the property.

Which browsers send the signal?

Measure rather than trust a list, since support moves between versions. Run probe.mjs against each browser on the machine and record the version alongside the result, as the Verified block below does.

Is GPC part of a web standard?

The Global Privacy Control specification is a W3C Working Draft, dated 11 June 2026. Its own status section says it is "inappropriate to cite this document as other than a work in progress". Legal obligations attach to the signal separately from its standards status.

Verified

Verified by Maks Vernycurl 8.21.0Node 22.23.2Chrome 152.0.7977.76Brave 1.95.101

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.

basic8 minpublished updated Maks Verny