canvas readback is not noise-injected (1×1 probe)
Check id canvas-tamper
What an ordinary browser yields
A pixel filled pure black reads back pure black.
What a detector infers
The probe fills a single pixel with pure black on a 1x1 canvas and reads it straight back with getImageData. An untouched pipeline returns exactly (0,0,0); a noise-injecting farble perturbs the value and it comes back non-black. This is a zero-baseline oracle — the expected answer is known a priori, so any site can run it in a few lines and needs no reference corpus. It distinguishes two very different properties: "this canvas output is unique" (ordinary, every machine differs) versus "this canvas output has been modified" (a deviation from the platform's defined behaviour). If navigator.brave is present and the pixel is perturbed, the check returns N/A and is not scored: Brave ships farbling as a stock privacy feature, so there the noise identifies the browser rather than indicating a lie.
How to resolve it
Where canvas output is scored, turn canvas noise off (fingerprint_noise=false / --disable-fingerprint-noise) or route readback through a real GPU with the canvas bridge, so the output is genuinely produced rather than perturbed after the fact. Randomised canvas output resolves a uniqueness problem while creating a coherence one.
Read in the wild by
CreepJS
CreepJS, an open research and education demo rather than a production detector, fills a canvas with random rgba values it records and reads the pixels straight back with getImageData to report which colour channels came back altered, and separately flags a cleared canvas whose pixels do not read back as zero.
src/canvas/index.ts `getPixelMods()` (L11-L126): fills an 8x8 canvas from `const red = ~~(Math.random() * 256)` (L49-51) via `context1.fillStyle = `rgba(${colors})`; context1.fillRect(x, y, 1, 1)` (L53-54), recording written colors with `pattern1.push(colors)` (L63); reads them straight back with `const { data: [red, green, blue, alpha] } = context1.getImageData(x, y, 1, 1) || {}` (L69-71) into `pattern2` (L93), then diffs written-vs-readback into `rgbaChannels` / `patternDiffs` (L97-98, L112-113). Separately, on a canvas cleared at L439: `if ((mods && mods.pixels) || !!Math.max(...context.getImageData(0, 0, 8, 8).data))` (L440) triggers `documentLie('CanvasRenderingContext2D.getImageData', 'pixel data modified')` (L442). Self-description, README.md L91: "The goal of this project is to conduct research and provide education, not to create a fingerprinting library."
Akamai
Renders text and gradients to a hidden canvas and hashes the pixels.
signal_categories.md - 3. Canvas Fingerprinting
Edioff/akamai-analysis — signal_categories.mdDataDome
Ships a canvas hash packed together with the screen dimensions and colour depth.
KurKFS = "8cd0430a…c60796_1920_1055_8"
glizzykingdreko/datadome-encryption — tests/original.jsonEvery attribution traces to a published artifact. See the sources and their limits.
Nearby checks in Render & GPU
- WebGL exposes a full parameter table + UNMASKED vendor/renderer
webgl-param-tableBeyond the two headline strings, a WebGL context answers getParameter for dozens of driver-specific limits. - WebGL vendor/renderer identical in a Worker
webgl-worker-vs-mainThe audit reads UNMASKED_VENDOR and UNMASKED_RENDERER on the main thread, then spawns a dedicated Worker that builds its own WebGL context… - masked WebGL VENDOR matches the engine
webgl-engine-vendorThis check reads the plain gl.getParameter(gl.VENDOR) — the masked value, not the debug extension's unmasked GPU string. - WebGL1 and WebGL2 report the same UNMASKED GPU
webgl1-webgl2-coherenceThe check creates a WebGL1 context and a WebGL2 context and reads UNMASKED_VENDOR_WEBGL / UNMASKED_RENDERER_WEBGL from each through the… - one flat fill reads back as exactly one colour
canvas-flat-fill-uniformThis is the area form of the 1×1 canvas probe, and it exists because that probe is easy to pass by accident. - one pixel reads the same through a full read and a windowed read
canvas-subrect-consistencyThe same pixels, asked for twice through different windows. A full-canvas read and an inner 16×16 read overlap on 256 pixels, and those are… - a canvas reads back the same through toDataURL and getImageData
canvas-todataurl-vs-getimagedatatoDataURL and getImageData are two exits from one canvas, and PNG is lossless, so decoding the data URL back into pixels has to reproduce… - one WebGL clear reads back as exactly one colour
webgl-readback-flat-uniformA single glClear writes one colour into every pixel of the framebuffer, so reading that framebuffer back must yield exactly one colour.
Clearcote is a browser built for fingerprint coherence
It is a Chromium fork, maintained by the same people who wrote this reference. It ships as a compiled browser rather than as a stealth script injected into someone else's — which is a description of how it is built, and is not an argument about how it behaves on this check.
This audit takes no position on how Clearcote scores on Render & GPU checks, on this one, or anywhere else. It has no baseline corpus of other people's fingerprints to rank you against and no vendor scoreboard — nearly every check is self-referential, asking one browser the same question through two independent APIs and reporting whether both answers can be true at once. It runs identically on any browser, including ours. Run it on yours and read the result yourself.
See the other checks in Render & GPU — the family canvas-tamper belongs to.