Skip to content

Fingerprint spoofing

Each session is assigned a device profile taken from a real device and spoofs more than 50 individual signals to match it. The values are set inside Chromium’s own code, and they agree with one another. Both parts decide whether a page loads.

Setting a valid User-Agent, patching canvas and WebGL, and stopping there is enough to be caught. Detection systems cross-reference signals against each other and look for a profile that describes one real machine.

A Windows User-Agent with macOS fonts is flagged. A phone screen resolution beside a desktop GPU is flagged. A Europe/Berlin timezone with an en-US locale and a US exit IP is flagged. Each value on its own is plausible; together they are not.

Patched in the engine, not injected into the page

Section titled “Patched in the engine, not injected into the page”

Most stealth tools run JavaScript in the page before the site’s own scripts, to overwrite navigator properties and wrap canvas calls. A page can find that work: an overwritten getter, a changed prototype or a function whose toString() is not native code are all visible from JavaScript, and detection scripts look for them.

rayobrowse changes Chromium’s C++ source instead. Blink, V8, the network stack, WebRTC and the GPU process are patched, so a spoofed value comes back from the same native code path a real browser uses. There is no injected script for a page to find.

  • One profile, every process. The profile is loaded once and handed to the renderer, web workers and the GPU process. The main page, iframes, workers and WebGL all describe the same device, so a worker never reports different hardware from the page that started it.
  • Canvas is real device output, not noise. A canvas read (toDataURL, getImageData, toBlob) returns what a real device with this profile renders for that drawing. The usual alternative is random noise, and noise is detectable: draw the same canvas twice and the pixels differ, which a real GPU never does.
  • Fonts belong to the claimed OS. The session gets that OS’s font set and never falls back to fonts from the machine it runs on, so a request for Arial never renders in a substitute. Font lists, document.fonts.check() and the default serif, sans-serif and monospace fonts all match, including for CJK locales.
  • Text measures like the claimed OS. Glyph widths and positioning follow the claimed OS’s text rendering, so width-based font probes return the numbers a real machine of that type returns.
  • WebGL down to the parameter table. The renderer and vendor strings, the full getParameter() set, the extension list and shader precision are set in the engine and describe one GPU. Extensions that GPU lacks report unsupported.
  • Timezone, locale and geolocation follow the proxy. At launch the browser looks up its own exit IP through the proxy and sets all three from it. Change the proxy’s country and Intl.DateTimeFormat().resolvedOptions().timeZone changes with it.
  • WebRTC reports the proxy’s exit IP, never the address of the machine the browser runs on.
  • An attached CDP client leaves no trace. navigator.webdriver is false. The side effects Playwright and Puppeteer normally leave in the page when they attach, such as automation markers in the page and side effects of the debugger connection, are removed.

The patch set tracks upstream Chromium and is carried forward to each new release, so the browser’s real behavior matches the version its user agent claims.

Each row covers several individual signals.

Category Covered
User-Agent and client hints Full UA string, Sec-CH-UA headers, userAgentData, platform and architecture hints
OS and platform navigator.platform, oscpu, platform-specific behavior differences
Screen and display Resolution, color depth, device pixel ratio, available area, media queries
WebGL Renderer, vendor, extensions, shader precision, the getParameter() table
WebGPU Adapter info and limits
Canvas Output a real device renders for the same drawing, not random noise
Fonts and text Font list, font availability checks, default font mappings and text measurement, all for the claimed OS
WebRTC Reports the proxy’s exit IP, media device enumeration
Timezone, locale, geolocation Set from the proxy’s exit IP, Intl and Date consistency
CPU and memory hardwareConcurrency, deviceMemory from realistic hardware profiles
Audio AudioContext output, sample rate, channel configuration
Speech The speechSynthesis voice list for the claimed OS
Media capabilities Codec and decoding support answers, DRM behavior
CSS system colors The claimed OS’s default colors
Language navigator.language, languages, Accept-Language
Media devices Camera and microphone enumeration
Battery Battery API state consistent with the device type
Network navigator.connection, downlink, effective type
Permissions Consistent permission states and feature policy responses
Plugins and MIME types Chromium-accurate plugin list and MIME support, doNotTrack
Visibility and focus With force_visibility=true, a background tab still reports visible and focused
Automation signals No navigator.webdriver, no Playwright or Puppeteer artifacts, no CDP traces

Every session draws a complete profile from a database of fingerprints collected from real devices. os, browser_version_min, browser_version_max, screen_width_min and screen_height_min narrow the draw, and everything else comes from the one device the draw lands on.

Two sessions with the same parameters get different devices. A test that asserts on navigator.userAgent or a pixel-exact screenshot fails on the second run for that reason. See Automated testing for what to pin.

os Coverage
windows The default, and the most tested
android Mobile profiles, well covered
macos Available, fewer profiles
linux Desktop Linux profiles. Available, fewer profiles

Don’t reach for os=macos expecting the same pass rate as Windows. The profile pool behind it is small.

os=linux presents as Chrome on a Linux desktop: the user agent says X11; Linux x86_64, and the GPU, fonts and screen describe the same machine.

browser_version_min and browser_version_max default to the major version of Chromium the browser is running. Leave them alone. Set them lower and the profile claims a version whose behavior the engine does not have, which is exactly the kind of cross-signal mismatch detection looks for.

For a test that needs the same device on every run, email [email protected].

Was this page helpful?