Skip to main content

Websites detect automation by looking at many browser signals: navigator.webdriver, user agent, platform, screen size, hardware concurrency, WebGL vendor, canvas noise, audio noise, time zone, geolocation, media devices, fonts, and more. seleniumbase-rs can spoof these signals through a coherent Fingerprint and a pluggable evasion system.

This page explains how to use built-in fingerprint presets, build custom profiles, say per setting whether the browser’s own value, a drawn value, a given value or nothing is wanted, choose how the claims are delivered, and apply native-level spoofing through CDP. It covers what a page can read inside the browser. The TLS and HTTP/2 side, and the scraper built on it, is a separate project now; see Stealth Scraping.

Info

What you will learn

  • How to use built-in fingerprint presets.
  • How to generate a random but coherent Fingerprint from a seed.
  • How to build a custom Fingerprint, one Spoof per setting.
  • What Natural, Random, Value and Off mean, and where Off exists.
  • How a StealthPolicy delivers the claims, and why it never changes a setting.
  • How to enable native-level CDP spoofing.
  • Which launch paths apply a fingerprint, and what each one does.
  • How an external profile payload maps onto all of this.
  • How the evasion provider registry assembles the JavaScript bootstrap.

Quick start#

A fingerprint reaches the page only through a launch path that applies it:

  • With DriverMode::Uc (above) or DriverMode::Cdp, the session registers the evasion script and sends the protocol overrides described under Native-level (CDP) spoofing, and the launch arguments apply. The script is the one the Pure CDP engine runs, evasions::session_script (the 27 providers of session_registry); see Built-in providers.
  • With the default DriverMode::WebDriver, only the launch arguments apply (user agent, --lang, --window-size, the WebRTC policy, and the QUIC and proxy flags). No script is injected and no protocol override is sent.
  • The Pure CDP engine and the Playwright session have their own paths, described below. The Pure CDP engine runs the same session_script. The Playwright session runs bootstrap_script, the 29 providers of default_registry.

One setting, four states#

Every setting of a fingerprint is one Spoof, and the state says everything about it. The value and the intent are one field, so they cannot disagree.

StateMeaning
NaturalThe browser’s own value, left alone. This is the default of every setting.
RandomA valid value, coherent with the rest, drawn when the fingerprint is built. Repeatable with .seed(u64).
Value(v)Exactly this value. A plain value converts to it: .timezone("Europe/Berlin") is Value.
OffThe API is turned off: unavailable, or denied. Only for geolocation, WebRTC, media devices, battery, Bluetooth, speech voices, network information and Client Hints.

build() returns an error that names the setting when Off is given to any other, so a time zone cannot be “off” by accident.

The random position is a spot in Berlin, because the time zone is a Value.

A built fingerprint is never changed. Its fields are private and are read through accessors (fp.timezone() returns &Spoof<String>); to change one, start a builder from it:

Fingerprint presets#

Built-in presets set a coherent combination of user agent, platform, screen, WebGL strings, a time zone, and the policy StealthPolicy::balanced():

PresetWhat it claims
Fingerprint::windows_desktop()Chrome 156 on Windows 10, 1920x1080, 8 cores, 8 GB, en-US, America/New_York, an NVIDIA RTX 4070 Ti through ANGLE and Direct3D 11.
Fingerprint::edge_desktop()The Windows preset as Microsoft Edge 156. The user agent ends with Edg/156.0.0.0, and the Client Hints name Microsoft Edge where Chrome names itself Google Chrome (randomize::edge_hints; Edge’s full versions are not in the Chrome table, so they are the frozen 156.0.0.0).
Fingerprint::macos_desktop()Chrome 156 on macOS (Macintosh; Intel Mac OS X 10_15_7), 1920x1080, America/Los_Angeles, and the WebGL strings Chrome reports on a Mac: Google Inc. (Apple) and ANGLE (Apple, ANGLE Metal Renderer: Apple M3, Unspecified Version). The user agent is Chrome’s, not Safari’s.
Fingerprint::linux_desktop()Chrome 156 on Linux x86_64, 1920x1080, America/Chicago, an NVIDIA RTX 4070 through ANGLE over OpenGL (Google Inc. (NVIDIA Corporation)), worded as a generated Linux identity words it.
Fingerprint::android_mobile()Chrome 156 on Android, in Chrome’s frozen user agent (Linux; Android 10; K), 412x915 at a device pixel ratio of 3, 5 touch points, Adreno 740.
Fingerprint::ios_mobile_safari()Mobile Safari 26.0 on an iPhone (iPhone OS 18_6, the iOS version Safari 26 freezes in its user agent), 390x844 at a device pixel ratio of 3, 5 touch points, Apple GPU. Safari has no battery, network information, Web Bluetooth or Client Hints, so those are Off, and it has no navigator.deviceMemory, so none is claimed.

Every preset also carries a battery, network information, the speech voices of its system and Web Bluetooth (where its browser has them). It leaves the position Natural on purpose: a position is answered without a prompt, and a fresh browser answers permissions.query for geolocation with prompt, so a preset that claimed one would stand out. validate() warns about a time zone without a position only when the policy grants permissions, which hands pages the real position. All six presets validate without errors or warnings; tests/mcp_webdriver.rs and the unit tests of stealth::fingerprint check it. A preset holds no Random, so it is the same every time and its canvas noise is the same on every launch.

Use the fingerprint! macro for shorter syntax. safari is an alias for ios, and edge is the Edge preset:

Randomised identities#

A preset is one identity; a fleet of sessions that all share it is easy to spot. Fingerprint::randomized(os, seed) generates a different, believable identity for each seed instead. It is the builder with the settings of a persona set to Random, so you can draw the same way for any setting yourself:

Detectors check that signals agree with each other, so the generator never randomises a field alone. It draws a whole machine and a whole place, then derives every field from them:

DimensionWhat is generated
WebGLVendor and renderer the OS really reports: ANGLE over Direct3D 11 (Windows), ANGLE over Metal with an Apple chip (macOS), ANGLE over Mesa or the NVIDIA driver (Linux), an Adreno or Mali chip (Android), Apple GPU (iOS).
HardwarehardwareConcurrency, deviceMemory, screen size, colorDepth and device pixel ratio that machine ships with: a Mac gets its chip’s core count and its model’s screen, a PC’s class decides its cores, memory and screens, and a phone gets its own screen.
PlaceA locale and language list, a time zone it is spoken in, and coordinates in that zone’s main city, so locale, timezone and geolocation agree.
SoftwareUser agent, platform, vendor and Client Hints (built the way Chromium builds them) that agree with the OS and with the claimed Chromium version.
ExtrasBattery, network information, speech voices and Web Bluetooth where the browser has them, and Off where it has not (Safari).
NoiseThe fingerprint’s seed feeds the canvas and audio noise of the policy, and every seed is different.

All five OsType values are supported, with StealthPolicy::balanced().

Things to know:

  • Stable within a release. A seed maps to the same identity for one version of the crate. Adding a table row changes what a seed maps to, so store the generated Fingerprint (it is Serialize), not the seed, if an identity has to outlive an upgrade.
  • The Chromium version is a fixed window. At this version of the crate the user agent claims Chromium 154, 155 or 156 (an iPhone identity claims Mobile Safari 18.1, 18.3.1, 18.5 or 26.0; Safari 26 names the frozen iPhone OS 18_6 in its user agent). Pair the identity with a browser of that era, or change user_agent, client_hints and core_version together. Fingerprint::randomized_with_major(os, seed, major) fixes the Chromium major instead (Safari and iOS ignore it), and so does .core_version(major) on a builder that draws a user agent or Client Hints; the stealth scraper of seleniumbase-rs-stealth uses it so the identity claims the Chrome that is running (see Stealth Scraping).
  • iOS differs. Safari has no navigator.deviceMemory and no navigator.userAgentData, so an iOS identity leaves device_memory Natural and turns client_hints Off.
  • Client Hints carry a real full version. The user agent keeps Chrome’s frozen major.0.0.0, but uaFullVersion and fullVersionList name a real build from randomize::KNOWN_FULL_VERSIONS: 154.0.8037.100 and 156.0.8078.35 from Google’s published stable releases, and 155.0.8059.40 as Chrome 155 reported it on a Mac (tests/client_hints_chrome.rs). A major the table does not list (one forced with randomized_with_major) falls back to major.0.0.0.
  • Platform versions. A Mac claims macOS 15.8.1, 26.7.1, 27.0.0 or 27.0.1, in the major.minor.patch form Chrome reports (27.0.1 was read from Chrome 155 on macOS 27.0.1). The Windows list (10.0.0, 13.0.0, 15.0.0, 19.0.0) follows Microsoft’s description of the value and was not read from a Windows Chrome.
  • Not drawn by randomized: fonts, media devices and WebRTC addresses. Ask for them with Spoof::Random on a builder; the proxy has no random value.
  • Applying it. For the Pure CDP engine, hand it to the launch options with LaunchOptions::builder().fingerprint(&fp) (see Pure CDP); page.apply_fingerprint(&fp) does the same for one tab. For BaseCase, pass it to BrowserConfig::with_fingerprint and use DriverMode::Uc or DriverMode::Cdp. You can also feed it to the evasion registry yourself.

Building a custom fingerprint#

For full control, use the builder:

There is no flag to flip afterwards, and none to set. Giving a value is the intent to use it, so the fingerprint above masks its navigator, language, screen, WebGL, time zone, position and media devices. Fingerprint::is_masked(Dimension) is the one question the page-script providers, the launch arguments, the protocol overrides and the proxy all ask, so a value is never half applied (a user agent sent to the browser but not to the page, say).

The settings#

Every setter takes impl Into<Spoof<T>>. screen(width, height) is the common case of screen_spoof. A tuple is the value of geolocation ((lat, lon)), webgl ((vendor, renderer)), webrtc ((public_ip, local_ip)), avail_screen and media_devices ((audio_in, audio_out, video_in)).

SettingRandom drawsOff meansDimensionWhat then acts
user_agent, platform, hardware_concurrency, device_memory, max_touch_points, vendor, product_sub, app_versionThe machine’s value for the operating system.no OffNavigatorThe navigator_props and plugins providers, --user-agent, Network.setUserAgentOverride.
oscpu, build_idnothing to draw (an error)no OffNavigatorThe navigator_props provider.
locale, languages, accept_languagesThe place’s locale and language list.no OffLocalizationThe localization provider, --lang, Emulation.setLocaleOverride, and the Accept-Language of the user agent override.
timezoneThe place’s time zone.no OffTimezoneThe timezone provider and Emulation.setTimezoneOverride.
screen_spoof, avail_screen, pixel_ratio, color_depthThe machine’s screen.no OffScreenThe window_geometry provider, --window-size and Emulation.setDeviceMetricsOverride.
webglAn adapter the operating system reports.no OffGraphicsThe webgl provider.
fontsThe system’s fonts, with a few of the less common left out.no OffFontsThe fonts provider.
media_devicesA microphone, a speaker and, usually, a camera.No device is listed.MediaDevicesThe media_devices provider.
geolocationA spot in the place’s main city.The request is denied (PERMISSION_DENIED).GeolocationThe geolocation provider, Emulation.setGeolocationOverride, and the geolocation permission for a position.
webrtcA public and a local address.RTCPeerConnection and the rest of WebRTC are not there.WebRtcThe webrtc provider and --force-webrtc-ip-handling-policy.
batteryA battery that is mostly charged.getBattery is not there.BatteryThe battery provider.
connectionA 4G connection.navigator.connection is not there.ConnectionThe connection provider.
speech_voicesThe system’s voices.No voice is listed.SpeechThe speech provider.
bluetooth (true: an adapter, false: none)Whether an adapter is there.navigator.bluetooth is not there.BluetoothThe bluetooth provider.
client_hintsClient Hints built the way Chromium builds them.navigator.userAgentData is not there.ClientHintsThe client_hints provider, and the metadata of the user agent override.
proxynothing to draw (an error)no OffProxy--proxy-server. A proxy without a host is an error.

A Natural setting changes nothing, so the providers define only the values the fingerprint holds.

How Random stays coherent#

build() works out the operating system (the explicit one, else the one the user agent names, else Windows), then the place: the region of a given time zone, else of a given locale, else a drawn one. Every Random setting is taken from one seeded draw of that machine and that place, so:

  • a random locale agrees with a given time zone, and a random time zone with a given locale;
  • a random position is in the main city of that time zone;
  • a random graphics adapter is one the operating system reports, and random Client Hints name the Chromium major the user agent names.

When nothing coherent can be drawn (a random locale next to a time zone the table of places does not know), build() returns a FingerprintError that says which setting and why. .seed(u64) makes the draw repeatable. Without it the draw uses fresh entropy, and the seed it used is kept in the fingerprint (fp.seed()), so fp.to_builder().build() reproduces it.

build() also fills two values the caller left out: languages, from a given locale (the table’s list for a known locale, else the locale and its language), and navigator.platform and the operating-system persona, from a recognised user agent. A value you set is never replaced.

The policy#

A setting is what the browser should claim. The StealthPolicy is how the claims are delivered and which protections run beside them. It has no say over a claim: a preset policy never changes a setting’s state, and the two can be given in any order.

PresetWhat it is
StealthPolicy::default()Only what the other evasions rely on: toString masked and shadow roots opened.
StealthPolicy::balanced()The default, plus the WebRTC leak policy DisableNonProxiedUdp, seeded canvas and audio noise, the window.chrome stubs and the headless fixes. Every preset and every randomized identity uses it.
StealthPolicy::native_only()Delivers the claims through CDP and launch arguments where the browser can (native_spoofing), with the WebRTC leak policy; no noise, no JavaScript-only stubs.
StealthPolicy::max()balanced(), plus tracker blocking, granted permissions and QUIC off.
Field (builder shortcut)What it controls
webrtc_policy (.webrtc_policy(..))--force-webrtc-ip-handling-policy. None passes no flag, unless the webrtc setting is set, which uses DisableNonProxiedUdp. A policy also runs the webrtc provider’s leak filter.
canvas_noise (.canvas_noise(..))The canvas_noise provider: Persistent (seeded), Random (a fresh seed for each document), Low (one pixel in eight) or Natural (none).
audio_noise (.audio_noise(..))The audio_noise provider: Persistent, Random or Natural.
native_spoofing (.native_spoofing(..))See Native-level (CDP) spoofing.
native_tostringThe native_tostring provider, on unless switched off. The other evasions rely on it.
chrome_runtimeThe chrome_runtime provider (the window.chrome stubs).
headless_fixesThe headless, permissions and hairline providers, and Emulation.setFocusEmulationEnabled.
grant_permissions (.grant_permissions(..))Browser.grantPermissions for notifications, midi, midiSysex, clipboardReadWrite, clipboardSanitizedWrite, paymentHandler, backgroundSync, idleDetection and webAppInstallation. A position grants the geolocation permission itself.
block_trackers, blocked_trackersThe tracker_block provider and Network.setBlockedURLs for Fingerprint::tracker_hosts(). A tracker list is itself the request to block them.
blocked_ports (.blocked_ports(..))Ports refused on localhost, 127.0.0.1 and [::1] with Network.setBlockedURLs, in the same command as the tracker hosts.
quic (.quic(..))Disabled and ForceHttp2 add --disable-quic; Enabled adds --enable-quic; Natural and Auto add nothing.
disable_csp (.disable_csp(..))Page.setBypassCSP.
open_shadow_rootsThe attach_shadow provider, which forces closed shadow roots open so automation can reach into them. A page can detect that, so turn it off where it matters.

StealthFlags is a deprecated alias of StealthPolicy, kept for one release.

Fingerprints as JSON#

A Spoof is written as "natural" (and left out), "random", "off" or the value itself; null reads as Natural. A fingerprint that holds "random" is drawn when it is read, the same way build() does it, so give a seed for a repeatable file. A field this version does not know, such as the flat screen_width of the old shape, is an error and not a silent loss. Battery times that are infinite (discharging_time of a charging battery) are written as null.

Native-level (CDP) spoofing#

When a session has a CDP connection (DriverMode::Uc, DriverMode::Cdp, or the Pure CDP engine), a fingerprint is applied in two layers:

  1. The evasion script, assembled from the providers below and registered with Page.addScriptToEvaluateOnNewDocument. Page scripts can in principle detect a JavaScript patch through Function.prototype.toString, Object.getOwnPropertyNames, or descriptor inspection, which is why the providers use the sb helpers to look native.
  2. The protocol overrides from evasions::safe_cdp_overrides. (The older evasions::cdp_overrides is deprecated: it also holds the Proxy-Authorization header that would hand the proxy’s login to every site.) A command is sent for a setting that holds a value, so a Natural setting sends nothing:
CommandSent when
Network.setUserAgentOverride (user agent, platform, userAgentMetadata, and acceptLanguage)user_agent is a value. acceptLanguage is sent only when accept_languages or locale is a value. The metadata comes from client_hints when set and is derived from the OS and user agent otherwise: Chrome’s brand list, or Edge’s when the user agent has an Edg/ token. client_hints Off sends none.
Emulation.setDeviceMetricsOverridescreen is a value. pixel_ratio is the scale factor (1.0 if unset) and mobile follows os_type. safe_cdp_overrides sets the viewport (width, height) to the screen, and the WebDriver path sends it as it is. The Pure CDP engine changes it for a desktop persona: it sends a 0 by 0 viewport, which the protocol reads as “no override”, plus screenWidth and screenHeight, so the browser itself reports screen.width and screen.height and the page keeps the viewport its window really has. A mobile persona is a phone-sized page, so its viewport stays the screen.
Emulation.setLocaleOverridelocale is a value.
Emulation.setTimezoneOverridetimezone is a value.
Emulation.setGeolocationOverridegeolocation is a position (accuracy 100 and altitude 0 unless set).
Browser.grantPermissionsThe geolocation permission, when geolocation is a position, and the permissions listed under grant_permissions when that policy field is on.
Network.setBlockedURLsOne command, because a second would replace the first: the tracker hosts when the policy blocks trackers, and *://localhost:PORT/* (and 127.0.0.1, [::1]) for each of blocked_ports. A session with ad blocking joins the ad block list to it.
Page.setBypassCSPdisable_csp is on.
Emulation.setFocusEmulationEnabledheadless_fixes is on.

So a UC or CDP session already gets these overrides without native_spoofing, and the JavaScript providers patch the same properties on top of them. native_spoofing makes the providers stand down where a protocol override covers the property, so the browser reports the value itself instead of a replaced JavaScript getter:

When native_spoofing is true:

  • navigator_props leaves out platform, userAgent and appVersion (a given app_version is still scripted).
  • window_geometry emits only colorDepth and pixelDepth; the size and scale come from Emulation.setDeviceMetricsOverride.
  • localization, timezone, the JavaScript geolocation provider that answers a position, and client_hints are skipped. A denied geolocation (Off) is still scripted: no protocol command denies the request for one tab.
  • The JS providers stay for everything the protocol does not cover: hardwareConcurrency, deviceMemory, maxTouchPoints, vendor, WebGL vendor and renderer, canvas and audio noise, media devices, fonts, battery, connection, speech, Bluetooth, the navigator.webdriver getter and the marker cleanup.
  • A WebDriver session (Chrome, Chromium or Edge) also launches a patched copy of the browser. If BrowserConfig::browser_binary_path is unset, the session finds the system Chrome (SB_CHROME_BIN, else find_system_chrome()), writes a patched copy through ChromeBinaryPatcher with EnginePatch::chrome_binary() (webdriver markers, navigator.webdriver assignments and the HeadlessChrome user-agent marker), and sets browser_binary_path to it. It returns an InvalidConfig error naming SB_CHROME_BIN if no Chrome is found. A browser_binary_path that is already set is used as it is and is not patched; that includes the one BrowserConfig::from_env() takes from SB_CHROME_BIN.

Caveats#

Warning
  • The protocol overrides and the script need a CDP connection. In DriverMode::WebDriver, neither is applied whether or not native_spoofing is set: only the launch arguments (and, with native_spoofing, the patched binary) take effect.
  • colorDepth and pixelDepth are not covered by the device metrics, so a minimal JS patch is still emitted for them.
  • Some signals (WebGL, canvas noise, audio noise) have no protocol override and stay JavaScript-based in native mode.
  • Whether a given detector can still tell the difference is not something this crate measures.

rustwright / Playwright mode#

Feature: playwright

This mode needs the playwright Cargo feature (rustwright); it is not on by default.

BrowserConfig::default() is headless; .with_headless(false) opens a window. (launch_with_fingerprint and launch_headless_with_fingerprint are deprecated shortcuts for this.) The session passes the fingerprint’s launch arguments, then evaluates the evasion script in the first page and again after every goto and new_page, because rustwright cannot register a script to run in each new document.

Warning

The script therefore runs after navigation, not before the page’s own scripts, so a page that reads the identity while it loads can see the browser’s real one. The protocol overrides listed above are not sent in this mode.

External profile payloads#

The profile_payloads::ProfileParams type maps to a Fingerprint via ProfileParams::to_fingerprint(). ProfileParams::to_browser_config(webdriver_url) returns a BrowserConfig with that fingerprint attached.

The returned mode follows the payload’s automation field: playwright and puppeteer give DriverMode::Cdp; selenium (the default when the field is absent) and the Firefox browser types give DriverMode::WebDriver; anything else gives DriverMode::Uc. The script and protocol overrides are applied only in Cdp and Uc mode, so a payload that keeps selenium is launched with the launch arguments only. The payload’s cmd_params become Chrome flags, except switches that run a program, load an extension, set the profile directory, a proxy or remote debugging, or ignore certificate errors; those are refused. The Pure CDP engine does not pass cmd_params at all.

The payload keeps its own format, because other tools write it: the flags are strings (ProfileFlags) and the nested sections (navigator, screen, …) hold values. Every key reads the same words and ends in one of the four states of a Spoof. The profile’s folder_id and name seed the draws, so a profile gets the same drawn values on every launch, and its core_version is the Chromium major that a drawn user agent and drawn Client Hints claim (not for the Firefox and Safari personas, whose core is not a Chrome version).

Words every key reads#

Keywords are case-insensitive. A flag the payload leaves out is auto. The table below is the one place that lists the words and the literal format of each key; this is what the groups of words do:

  • The natural words give the browser’s own value, whatever values the payload holds for it.
  • auto (or no flag) gives the value the payload holds for the setting, and the browser’s own where it holds none.
  • The mask words give the value the payload holds, and a coherent drawn one where it holds none.
  • The off words turn the API off. Only where an API can be off (geolocation, WebRTC, media devices); for any other key they are read as Natural, and logged.
  • A literal is the value, in the format of its key. The other settings of the same key then follow the mask words: a held value, else drawn.

An unknown keyword is logged and read as Natural. So is a literal that is not in the format of its key (wibble for a time zone, a user agent that does not open with Mozilla/ and a digit). It is never guessed at: it used to switch the dimension on and fill it with generic values.

Keywords in JSON payloads#

One table for every key. The first row says what the words mean; each later row names only what the key adds or does differently, and the literal formats it reads.

Payload keyNaturalRandomValue (literal)Off
(the words above)natural, native, pass_through, whitelist; auto with no value heldmask, spoof, custom, generic, random, noisea value of the keyoff, disabled, block, empty
navigator_maskingthe common wordsthe common words, desktop_chrome, mobile_safari: user agent, platform, cores, memory, touch points and vendora user agent: Mozilla/ and a digit, with the platform in parentheses (Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...); it sets user_agentread as Natural
localization_maskingthe common wordsthe common words: locale, languages, accept-languages of the placea locale (de-DE, zh-Hant-TW) or an Accept-Language list that opens with one (en-US,en;q=0.9): the first tag is the locale, the tags the languages, the text the accept_languagesread as Natural
timezone_maskingthe common wordsthe common words: the place’s zonean IANA name, area and city: America/New_Yorkread as Natural
screen_maskingthe common wordsthe common words: size, pixel ratio and colour depth of the machine1920x1080 or 1920x1080x24 (24 when omitted); it sets screen and color_depthread as Natural
graphics_maskingthe common wordsthe common words: an adapter the OS reportsvendor~renderer, both given: Intel Inc.~Intel(R) Iris(R) Xe Graphicsread as Natural
fonts_maskingthe common wordsthe common words, system_only, blacklist: the system’s fontsa comma-separated list of fonts: Arial,Calibri,Consolas. One font is no list: give it in fingerprint.fontsread as Natural
media_devices_maskingthe common wordsthe common words2_mic_1_cam, or kind:id:label|kind:id:label (audioinput:default:Mic1|videoinput:default:Cam1): the counts and the labelled devicesthe off words: no device is listed
geolocation_maskingthe common wordsthe common words: a spot in the place’s citylat,lon or lat,lon,altitude (accuracy 20): 40.7128,-74.0060,10.5the off words: the request is denied
webrtc_maskingthe common words (and no IP policy)the common words: a public and a local addresspublic:IP|local:IP, IPv6 in brackets: public:172.56.21.89|local:10.0.0.5; it also passes the policy PublicAndPrivateInterfacesthe off words: WebRTC is unavailable
proxy_maskingthe common words, direct: no proxynone: mask, custom, socks5, http, https use the payload’s proxy section, as auto doesa socks5://, http:// or https:// URL: socks5://user:pass@host:1080 (port 1080 for SOCKS5, 8080 otherwise)read as Natural, and logged: a proxy has no off state
geolocation_popupprompt: ask, whatever position the payload holdsnot usednot usedblock: deny, whatever position the payload holds; allow and auto follow geolocation_masking
audio_maskingthe common words and the off words: no audio noiserandom: fresh noise for each script; mask, spoof, custom, generic, noise: noise seeded by the profilenot usednot used
canvas_noisenatural, auto, off, disabled: no canvas noiserandom: fresh noise for each scriptlow: noise on one pixel in eight; persistent, mask, spoof, custom, generic, noise: seeded noise. If the key is absent: seeded noisenot used
ports_maskingthe common words, the off words: nothing blockedthe common words, block, block_all, stealth: the ports of the payload’s fingerprint.ports are blockedblock:80,443 or 80,443: those ports (the policy’s blocked_ports)not used
quic_modenatural, autonot usedenabled, disabled, force_http2. If the key is absent: disablednot used

The audio, canvas, ports and QUIC keys set the policy and not a setting, so their “states” are the values of the policy field in the same columns. graphics_noise and startup_behavior are accepted and ignored. The booleans disable_csp, grant_permissions and native_spoofing set the same fields of the policy.

The settings of one key follow the word together: navigator_masking: "mask" with a navigator section that gives a user agent, a platform and a core count keeps those three and draws the memory, the touch points and the vendor, in the same machine. A key whose draw cannot be made (a random locale next to a time zone the table of places lacks) is logged and left Natural.

A literal in a flag is the value for the setting it names (the Value column above). A section of the payload that gives the same setting wins over a literal, and a section value that is an empty string is a value the payload does not give. What the browser is launched with (to_browser_config) and what the runtime overrides set (apply_runtime_overrides) are the built fingerprint’s: a natural flag launches without a --user-agent, a locale or a proxy, whatever the payload holds.

webrtc_masking also sets the IP-handling policy that keeps WebRTC from leaking the real address: every word but the natural ones passes disable_non_proxied_udp (including a payload that says nothing), public_only passes default_public_interface_only, and a literal address passes default_public_and_private_interfaces.

Payloads from older versions#

A payload written for an older version loads unchanged, with the words above. The differences you can see: an absent flag is auto (it was mask for the navigator, localization, time zone, graphics, fonts, screen, geolocation and WebRTC flags, which filled in generic values nobody asked for); off and disabled turn geolocation, WebRTC and media devices off instead of leaving them natural; an unknown word is Natural instead of mask.

Provider architecture#

Every evasion is an EvasionProvider: a small, self-contained unit that returns a JavaScript snippet for an EvasionContext (the session fingerprint plus a deterministic seed and runtime config). Providers are held by an EvasionRegistry and assembled into a single bootstrap script in priority order (lower priority() runs first). The script is one function; each snippet sits in its own try/catch, after a // provider: <name> comment. EvasionConfig turns either of those off.

evasions::bootstrap_script(&fp) is a thin wrapper over default_registry().bootstrap(&EvasionContext::new(&fp)), so existing callers keep working while new evasions are added centrally. evasions::session_script(&fp) is the same over session_registry, the script a browser session runs.

Adding your own evasion#

Implement EvasionProvider

Implement EvasionProvider for a unit struct. Return the JavaScript from script, gate inclusion with applies, and order it with priority. providers::SB_PREAMBLE brings in the sb helpers that make a replaced function look native; the example below skips it for brevity.

Register it at runtime

See Contributing for adding a built-in provider.

Built-in providers#

default_registry() registers 29 providers, listed here in execution order. “Runs when” is the provider’s applies condition. A dimension “is on” when Fingerprint::is_masked says so: a setting that belongs to it is not Natural, or, for the dimensions without a setting of their own, the policy switches it on (see One setting, four states).

The Pure CDP engine and the WebDriver Uc and Cdp modes do not use default_registry(). They use evasions::session_script(&fp), which is session_registry(&fp): the same list without plugins and connection (SESSION_OMITTED_PROVIDERS), which is 27 providers. A real Chrome already has navigator.plugins, navigator.mimeTypes, navigator.connection and the PDF viewer flag, and answers for them itself, so a replaced getter is one more thing a page can find that a real Chrome does not have. The assembled session script never mentions plugins, mimeTypes, pdfViewerEnabled or connection. default_registry() is unchanged, for callers that want those spoofs, such as a browser that really lacks them, and it is what evasions::bootstrap_script and the Playwright session use.

PriorityNameRuns whenWhat it does
5native_tostringthe policy’s native_tostring is onMakes replaced functions print [native code] from Function.prototype.toString, and installs the sb helpers the other providers use.
10webdriveralwaysMakes navigator.webdriver read false (a getter on the navigator prototype) and deletes known automation hooks from window (callPhantom, _phantom, __nightmare, __selenium_unwrapped, __webdriver_evaluate, __driver_evaluate, domAutomation, and others).
15cdp_markersalwaysDeletes $cdc_, __webdriver, __selenium, __driver, $chrome_ and __fxdriver properties from window, its prototype chain and document.
20chrome_runtimethe policy’s chrome_runtime is onwindow.chrome.app, loadTimes, csi and runtime stubs.
30navigator_propsNavigator is onuserAgent, appVersion and platform (not under native_spoofing), then hardwareConcurrency, deviceMemory, maxTouchPoints, vendor, productSub, and oscpu and buildID, each only when it is a value. A given app_version is scripted even under native_spoofing. It does not touch pdfViewerEnabled, plugins or mimeTypes: Chrome answers for them itself.
35permissionsthe policy’s headless_fixes is onMakes permissions.query({ name: 'notifications' }) agree with Notification.permission.
40pluginsNavigator is onFive PDF-viewer plugins and their MIME types. In default_registry() only.
45window_geometryScreen is onscreen.width, height, availWidth, availHeight, colorDepth, pixelDepth, plus outerWidth, outerHeight and devicePixelRatio, each only when it is a value. avail_screen sets the available size; without it it is the screen less 40 pixels in height. The window is as large as the persona’s screen, and never smaller than the viewport it really has. Under native_spoofing, only colorDepth and pixelDepth.
50webglGraphics is onUNMASKED_VENDOR_WEBGL and UNMASKED_RENDERER_WEBGL from WebGL1 and WebGL2 getParameter. A half the WebGl value lacks stays the browser’s.
55canvas_noisethe policy’s canvas_noise adds noiseDeterministic noise, a function of the seed and the pixel position, on toDataURL, toBlob and getImageData. Low touches one pixel in eight and Random draws a new seed for each script.
60audio_noisethe policy’s audio_noise adds noiseDeterministic noise on AudioBuffer.getChannelData and copyFromChannel and on AnalyserNode.getFloatFrequencyData, seeded by the fingerprint’s seed (a fresh one for Random).
65webrtcwebrtc is set, or the policy names a WebRTC IP policyOff: removes RTCPeerConnection, RTCSessionDescription, RTCIceCandidate and the rest of WebRTC. Otherwise drops STUN and TURN servers from RTCPeerConnection configs, and rewrites IPs in SDP and RTCIceCandidate for the addresses the value holds.
70batteryBattery is onnavigator.getBattery() from the BatteryProfile; Off removes it.
72connectionConnection is onNetworkInformation effectiveType, rtt, downlink and saveData from the ConnectionProfile; Off removes connection. In default_registry() only.
75media_devicesMediaDevices is onenumerateDevices, from the labelled devices or generated from the input and output counts; Off lists none.
76fontsfonts is a non-empty listLimits document.fonts.check, load and FontFace to the configured fonts.
78speechSpeech is onspeechSynthesis.getVoices() from the voices; Off lists none.
80bluetoothBluetooth is onA navigator.bluetooth stub whose getAvailability answers the value, only if the browser has none; Off removes navigator.bluetooth.
85headlessthe policy’s headless_fixes is onmatchMedia answers (prefers-reduced-motion: reduce and prefers-color-scheme: dark false, any-pointer: fine true) and document.hidden / visibilityState.
87prepare_stack_traceneverReserved name. It emits no script.
88client_hintsClientHints is on, and not native_spoofingnavigator.userAgentData, including getHighEntropyValues, but only in a secure context (window.isSecureContext): that is the only place Chrome has the property, so on a plain http: page there is none to read and none is added. localhost and 127.0.0.1 are secure contexts. Off removes userAgentData.
89media_codecsalwayscanPlayType answers for MP4, AAC and MP3 types when the browser says it cannot play them. WebM and everything else stay the browser’s own.
90timezonetimezone is a value, and not native_spoofingIntl.DateTimeFormat and the local-time methods of Date (getTimezoneOffset, getHours, toString, the toLocale*String family and others). new Date(y, m, d) and Date.parse of a string without an offset keep the machine’s zone.
92localizationLocalization is on, and not native_spoofingnavigator.language and navigator.languages: the languages the fingerprint holds, else its accept_languages, else the list its locale implies.
95geolocationgeolocation is a position and not native_spoofing, or it is OffgetCurrentPosition and watchPosition. They answer in JavaScript with the position, and refuse with PERMISSION_DENIED for Off.
110hairlinethe policy’s headless_fixes is onModernizr’s hairline check: offsetHeight of the element with id modernizr reads 1.
120iframeneverReserved name. It emits no script.
125attach_shadowthe policy’s open_shadow_roots is onForces Element.attachShadow to mode: 'open'.
130tracker_blockthe policy blocks trackersMakes fetch, XMLHttpRequest and navigator.sendBeacon to the tracker hosts (and their subdomains) fail like an unreachable host. Scripts, images and frames loaded by markup are not covered.

List them at runtime with default_registry().provider_names() (all 29, including the two reserved ones), session_registry(&fp).provider_names() (the 27 a browser session runs), applicable_names(&fp) (those that run for a fingerprint), or over MCP with the list_evasion_providers tool, which lists the session ones.

Profile coherence validation#

Fingerprint::validate() returns a CoherenceReport with errors and warnings; is_coherent() is true when there are no errors. Only values that were given are checked: a Natural setting is the browser’s own.

It reports an error for:

  • a navigator.platform that does not fit os_type (a macOS user agent with a Win32 platform), and a user agent with no token for os_type;
  • a screen with a zero dimension;
  • a WebGL renderer from another operating system’s graphics API: Direct3D off Windows, Metal or an Apple GPU off macOS and iOS, Mesa off Linux, and Adreno, Mali or PowerVR off Android;
  • a MobileSafari persona whose user agent has a Chrome/ token.

It reports a warning for:

  • a mobile user agent with maxTouchPoints == 0, and a desktop user agent with touch points;
  • a locale that differs from the first of languages;
  • a time zone with no geolocation (a geolocation that is Off is a choice, and does not warn);
  • a locale from another country than the time zone, judged with the table of places randomize::LOCALES (a zone the table does not hold, or a locale with no region, is not judged).

It does not look at hardware.

The MCP validate_fingerprint tool returns the same report for one of the six built-in presets (windows, edge, macos, linux, android, ios); it does not take an arbitrary fingerprint.

tests/fingerprint_randomized.rs asserts that Fingerprint::randomized gives no errors and no warnings for every OS, for seeds 0 to 1499 and ten edge seeds (0, u64::MAX, 1 << 63 and others). If you edit a generated profile with to_builder(), run validate() again afterwards.

Humanized input#

Detectors look for input that is too regular. The stealth::behavior module plans input that is not: curved pointer paths with a bell-shaped speed profile, Fitts’s-law timing, tremor and overshoot, and Gaussian typing with word and sentence pauses. It is pure and seedable:

To play a plan out on a page, use page.human(behavior) from the Pure CDP engine (see Pure CDP). BaseCase::human_type uses the same typing model, taking its pace from Fingerprint::humanize() when the configuration has a fingerprint.

Behavior::from_config(fingerprint.humanize()) turns a fingerprint’s HumanizeConfig (its minimum and maximum keystroke delay) into the matching behaviour.

Tip

The older stealth::humanize helpers (bezier_mouse_path, keystroke_delays) remain, but they sample a Bézier curve at evenly spaced parameter values and draw delays uniformly between two bounds; prefer behavior.

What is spoofed#

With a CDP session and the balanced() policy (see the provider table for the conditions), for the values the fingerprint holds:

  • navigator.webdriver reads false, and known automation hooks are deleted
  • navigator.userAgent, platform, hardwareConcurrency, deviceMemory, maxTouchPoints, vendor, language and languages
  • window.chrome.app, csi, loadTimes and runtime stubs
  • navigator.plugins and navigator.mimeTypes (bootstrap_script and the Playwright session; the session script leaves them to Chrome)
  • navigator.permissions.query for notifications
  • screen.width, height, availWidth, availHeight, colorDepth, outerWidth, outerHeight and devicePixelRatio
  • WebGL UNMASKED_VENDOR_WEBGL and UNMASKED_RENDERER_WEBGL
  • Canvas toDataURL, toBlob and getImageData noise
  • Audio noise (when the policy’s audio_noise is not Natural, which balanced() and so every preset and every randomized identity set)
  • navigator.mediaDevices.enumerateDevices (when media_devices is a value)
  • Time zone, through Intl.DateTimeFormat, the local-time methods of Date, and Emulation.setTimezoneOverride
  • Geolocation, through Emulation.setGeolocationOverride, the JavaScript provider, and the geolocation permission
  • $cdc_, __webdriver, __selenium and similar marker properties
  • Battery Status, Speech Synthesis, and Bluetooth APIs, and Network Information (the last only in bootstrap_script)
  • Client Hints (navigator.userAgentData), when client_hints is a value and the page is a secure context; Off removes it, as it removes getBattery, navigator.connection, navigator.bluetooth and WebRTC
  • matchMedia answers and page visibility, canPlayType for MP4/AAC/MP3, and attachShadow roots forced open
  • STUN and TURN servers dropped from RTCPeerConnection configs, and Chrome’s WebRTC IP-handling flag
  • Launch arguments such as --disable-blink-features=AutomationControlled
  • Optionally (native_spoofing), fewer properties replaced in JavaScript, and a patched Chrome binary for WebDriver sessions

Complementary defenses#

Runtime fingerprints work best when the browser also hides engine-level automation markers:

  • Patch the chromedriver binary with ChromedriverPatcher (for example ChromedriverPatcher::new(path).patch(EnginePatch::all())?) to remove injected cdc_ and __webdriver signatures. See the Binary Patching tutorial.
  • Pass the extra Chromium flags returned by engine_spoofing_args() (a Vec<String>) through BrowserConfig::with_extra_args, which replaces the list (push_extra_arg adds one), or through StealthOptions::extra_args.
  • Combining a Fingerprint, UC mode, binary patching, and engine args covers the most layers this crate offers. None of it guarantees that a given detector passes.

Limitations#

Warning
  • Nothing on this page changes the TLS or HTTP/2 behaviour of the browser. A fingerprint reaches the page through scripts and protocol overrides; the ClientHello, the HTTP/2 connection start and the header order that leave the machine are those of the browser you launch. JA3, JA4, peetprint and Akamai parsers, a measured table for one browser (Chrome 155 on macOS), a client that sends that handshake or one you specify, and MitmProxy, which gives a real Chrome on the Pure CDP engine the impersonated handshake, are in the separate, experimental seleniumbase-rs-stealth crates since 0.3.0; see Stealth Scraping. The design history is the Stealth and impersonation plan.
  • Some dimensions (WebGL vendor/renderer, canvas/audio noise, media devices, fonts, battery, etc.) have no CDP override and remain JavaScript-based even when native_spoofing is enabled.
  • Chromium source patching is not performed. The binary patchers edit byte strings in an existing chromedriver or Chrome binary.

Troubleshooting#

Still flagged by bot detection

Likely cause. The evasion script was never installed (DriverMode::WebDriver), the setting is Natural, or the site checks what this page does not change: the browser’s own TLS and HTTP/2 fingerprint (see Stealth Scraping), the IP address, the browser’s real version.

Fix. Use DriverMode::Uc or the Pure CDP engine, build the fingerprint from a preset or Fingerprint::randomized, check fp.is_masked(Dimension::...) for the dimension, and match the egress IP to the identity’s place.

A value was given and the page does not show it

Likely cause. A later call for the same setting replaced it (Natural included), or the profile payload’s flag for it is a natural word, which beats the values the payload holds.

Fix. Read the setting back (fp.timezone()), and remove the later call or the natural word.

`build()` fails with "cannot be Off" or "cannot be Random"

Likely cause. Off was given to a setting with no off state, or Random to one with nothing to draw (proxy, oscpu, build_id).

Fix. Give a value or leave it Natural; the message names the setting.

`build()` fails with "no coherent random"

Likely cause. A random locale, language or position next to a time zone or locale the table of places lacks.

Fix. Give the locale and the position as values, or use a zone the table holds (randomize::locale_of_timezone).

`navigator.webdriver` still `true`

Likely cause. The bootstrap script did not run: DriverMode::WebDriver, or no fingerprint on the config. The webdriver provider has no flag that turns it off.

Fix. Use DriverMode::Uc or Cdp, or Page::apply_fingerprint on the Pure CDP engine. For driver-level markers, see binary patching.

Geolocation is wrong or never answers

Likely cause. The geolocation setting is Natural or Off, or there is no CDP session.

Fix. Set .geolocation((lat, lon)) and use DriverMode::Uc or Cdp. The position grants the permission itself; Off refuses it on purpose.

Inconsistent locale and time zone

Likely cause. locale and timezone come from different countries. validate() warns about this pair.

Fix. Align both, or use Fingerprint::randomized, which draws them from one place.

WebRTC leaks local IP

Likely cause. The policy has no webrtc_policy or a looser one, and the webrtc setting is Natural.

Fix. Use StealthPolicy::balanced() (the presets do), which keeps WebRtcPolicy::DisableNonProxiedUdp, or turn WebRTC Off.


This page was brought up to date with the change that makes a setting one Spoof and moves the behaviour into the StealthPolicy by reading src/stealth/**, src/sb_cdp/{identity,launch}.rs, src/browser/{config,session,playwright}.rs, src/profile_payloads/**, src/mcp/stealth.rs and src/macros.rs again. The code examples were compiled against that source as a test of the crate; the tables were checked against the applies conditions of the providers.