Skip to content

Latest commit

 

History

28 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Fingerprint Shield

One coherent invented machine — the same one in the window, in every frame and in every worker.

CI version Chrome MV3 suites runtime dependencies

English · Русский

Coherence is the whole point. A site does not identify you by any single value; it identifies you by values that agree. Claiming a 1366×768 screen while the layout proves the window is wider, or answering matchMedia from a profile while the CSS engine answers from the real display, does not hide a machine — it produces a machine that cannot exist, and that is rarer than the one you started with.

Every fix here carries a measurement, and every suite compares the result against a clean browser rather than against an assumption.

The Who Am I page: fingerprint hash, platform, language and timezone, then the invented hardware, screen, GPU, canvas and WebRTC state, and the list of active modules

Who Am I — the machine as a site reads it. Every value on this page is the claim, not the host, and the fingerprint hash at the top is what a tracker would key on.

Install

Not in the Chrome Web Store — load it unpacked.

  1. Download the zip from Releases and extract it. CI builds it from the commit the tag points at, and it contains the extension only: no dev pages, no suites, no generators.
  2. Open chrome://extensions/ and turn on Developer mode.
  3. Click Load unpacked and point it at the extracted folder.

To run from source instead, point Load unpacked at a clone of this repository.

The extension's interface follows your browser's language, in English or Russian. The audit page, this README and the code comments are in English only.

What it looks like

The extension popup: protection active, six of six modules, the exit country, per-site WebRTC, Service Worker and CSP switches, the normal or stealth mode selector and the device profile

The popup: country, per-site switches, device profile.

The protection modules grid in the options page: Canvas, WebGL, WebRTC, Navigator, Screen, Timezone, Geolocation, Battery, Fonts, ClientRects, Plugins, Network and Hide AdBlock, each a checkbox with a one-line description

Thirteen modules, switched one by one. ClientRects ships off — it is the one that makes CreepJS go red.

The interface is in English and Russian and follows the browser's own language — there is no switch to set. A key missing from a catalogue leaves the text that is already there and says so on the console, because chrome.i18n.getMessage answers an unknown key with an empty string and writing that into the page would blank the control rather than fail. test/i18n.mjs opens the browser twice, --lang=en-US and --lang=ru, and requires the two renderings to differ — without that, a build ignoring the locale passes every other check by showing one language twice.

How it is verified

npm ci
npm test           # the 9 Node suites — seconds, no browser
npm run test:all   # adds the 48 Playwright suites — six to eight minutes

57 suites in total. The Node half runs on every push and every pull request; it includes test/parity-static.mjs, which re-runs both generators in memory and fails if mw-bundle.js or dyn/ on disk are stale. The Playwright half loads the extension for real in Chromium and is triggered manually, because its assertions are Windows facts — the ANGLE renderer strings, the font list, outerHeight - innerHeight.

Two are worth naming. clean vs ours diffs a patched browser against an unpatched one and fails on any difference nobody has judged. timezones vs ICU checks 941 timezone assertions against the ICU that Node bundles, which is why the Node version is pinned.

The population reference in tools/crowd-reference.json is read off named public sources on a named date — StatCounter for screen resolution, the Steam hardware survey for GPUs — with each source's sampling bias written down beside it. Nothing there is interpolated or rounded to taste. Where a number has no source, the tool prints a dash instead of guessing.

Limits

What this extension knowingly does not close. The rule for being on this list: a site can read it, and we know we cannot stop it — either the platform does not allow it, or closing it would cost more than the leak. The item numbers are referenced from the code and from the audit page, so they are not renumbered.

  1. The machine is 2.6 bits. There are six table profiles. A site reading only hardware — screen, cores, memory, GPU — sees one of six machines.

  2. Audio and text metrics are the host's. OfflineAudioContext computes on this machine's real audio stack. Text metrics are noised; audio is not.

  3. WebGL readback is noised, not controlled. Noise is not substitution: a site holding a reference for this GPU sees "not that one", not "this other one".

  4. The GPU is spoofed at the string level. UNMASKED_RENDERER_WEBGL and GPUAdapterInfo answer from the profile; anything actually computed on the card does not.

  5. A site's own service worker reads the real machine. Measured on one page: 16 cores against 18, Europe/Berlin against Europe/Moscow.

  6. On a trusted-types origin the machine becomes the host's — coherently. Creating a policy raises a violation that the browser builds in C++ from the real stack and names the extension in; JS cannot reach that.

  7. The screen becomes the host's when the window is wider than the claim. A screen cannot be smaller than the window, and one 100vw proves the lower bound.

  8. @media and matchMedia disagree on several features. matchMedia answers from the profile; the CSS engine answers from the real window, and the CSS engine is out of reach. The pixel-ratio half is closed since 2.5.27 — a claimed dpr the engine disproves is refutable in two lines, so the ratio yields to the host as the screen does above.

  9. The install window. For the first seconds after install or reload, dynamic DNR rules are not registered yet. The headers that leaked there — accept-language, which is a country — have been moved into the static ruleset and are now closed.

  10. A canvas read by an inline script during parse gets the defaults, because the feature decision freezes on first use.

  11. Window geometry before the browser knows it. In the first inline script outerWidth/outerHeight are 0. We pass that 0 through, exactly as a clean browser does.

  12. Exit country is the one axis internal coherence cannot see. Every suite asks whether the build contradicts itself. None can ask whether the claimed country matches the network the traffic actually leaves from.

  13. First visit to an origin that refuses blob workers. Every worker this extension patches is built from a blob; an origin whose CSP bars blob: rejects that construction.

  14. TLS and HTTP/2 are out of reach. Cipher order, TLS extensions, ALPN, curves, the HTTP/2 SETTINGS frame — JA3/JA4 and the h2 fingerprint are formed before the page gets a byte, and Cloudflare and Akamai read them as a matter of course.

  15. The WebRTC decoder and codec list are the graphics card, not a table. decodingInfo() answers powerEfficient from whether the real card has a hardware decoder; getCapabilities('video') lists video/H265 only where the hardware does HEVC.

  16. A WebGL warning about an unknown constant names us. Chrome attributes INVALID_ENUM to the nearest script frame, which is our wrapper.

  17. The marker names are fixed. '__t0' in window answers yes for this build and no for a clean browser, in the top document and in frames, and __AFP_PATCH_URL is the worker’s equivalent. They are non-enumerable, so a name diff against a fresh iframe does not show them — but a constant anyone can guess once needs no diff. Deriving them per site is blocked by the markers being set before the seed exists and by the in-browser checks that read them. There were two names on the window until 2.5.27; the second is a field of the first now. Hiding either was measured as worse than owning one fewer — a clean window has zero own symbols, so a symbol key makes the count anomalous and Symbol.keyFor hands the name back, while hiding from enumeration alone makes reachable, listed and in disagree, which no browser does for any name.

  18. The Intl locale and navigator.language answer to different switches. The language claim belongs to the navigator module; the window's Intl locale is installed under the timezone one. With navigator off the locale follows it back to the host, but the mirrored case is open: with the timezone module off no Intl wrapper is installed at all, so the window answers with the machine's locale while a worker answers with the profile's.

  19. The claimed core count is a number; parallelism is behaviour. navigator.hardwareConcurrency answers with the profile while the machine still runs as many workers at once as it really has. An extension cannot refuse the ninth the way an eight-core processor would — the scheduler belongs to the engine.

  20. The claimed memory size is not backed either. navigator.deviceMemory reports the profile's bucket while a page can allocate and watch for the bend that never comes. Same reason: the allocator belongs to the engine. Both of these are silent, need no permission, and are readable by any page that thinks to look.

  21. One sessionStorage key is visible. The per-document Trusted Types verdict lives at v.ui.tte. It claims nothing about the machine — it decides who is named in a refusal — but on a fresh origin a clean browser has no keys at all, so sessionStorage.length reading 1 instead of 0 finds it without guessing the name. It stays because it has to survive navigation: a previous document's verdict for the same route is what keeps the proxies from installing on a repeat load.

The extension's own audit page carries the same list beside its verdict, because a green verdict is only ever as broad as the questions asked.

Re-measure it yourself

node tools/probe-diff.mjs     # what the build CHANGES against a clean browser
node tools/probe-time.mjs     # what changes over TIME and does not for a clean browser
node tools/probe-crowd.mjs    # the size of the crowd you land in
node tools/diff-metrics.mjs   # two machines: what differs between them
node test/hostleak.mjs        # host values that make it through

Build

npm run build         # regenerates dyn/ and mw-bundle.js
node tools/pack.mjs   # builds dist/ — the extension only, 243 files
node tools/shots.mjs  # retakes the screenshots above from the running extension

mw-bundle.js is generated from the eleven modules in mw/. Edit the modules, not the bundle; npm test fails if the bundle on disk is stale.

Contributing

Bug reports and pull requests are welcome — see CONTRIBUTING.md. Found a security problem? Report it privately rather than in a public issue.

License

MIT.

About

Chrome MV3 anti-fingerprinting extension: one coherent invented machine across window, frames and workers. Interface in English and Russian, following the browser. Every fix carries a measurement, every suite compares against a clean browser

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages