Browser fingerprinting in Headless Chromium

Browser fingerprinting is an advanced technique that attempts to uniquely identify a user by collecting the characteristics of their browser and device. Even if you hide your IP address with a proxy, many combinations of features such as your browser’s user agent, screen size, language, time zone, hardware information, fonts, WebGL/Canvas outputs can give you away. These types of fingerprint checks are one of the methods used by websites to detect bots and automation tools.

Headless Chromium is a programmatic mode of the Chrome browser that runs without an interface. However, headless mode had some differences that gave it away in the past. For example, older versions of headless Chrome explicitly stated HeadlessChrome in the user agent and returned an empty navigator.plugins list. These differences made it easier for anti-bot systems to tell when a browser was running in headless (automation) mode.

In recent years, the Chromium team has minimized these differences with the “new headless mode.” The –headless=new mode that came with Chrome 109+ has largely closed the fingerprint differences by behaving like a normal Chrome even when running headless. For example, in the new headless mode, the HeadlessChrome phrase in the user agent text has been removed, and navigator.plugins has now started to return PDF viewer plugins like in the real browser. However, in order to completely hide the automation, developers may need to spoof (replace with fake but consistent values) the browser fingerprint data with their own profiles. In this article, we will examine how browser fingerprint data can be spoofed based on the user profile when using Headless Chromium on Android and desktop platforms. We will focus on methods and examples that are technically correct and overlap with real-world applications.

Browser fingerprinting in Headless Chromium

Headless Chromium and Fingerprint Detection

A web page can be richly fingerprinted by running JavaScript in the browser and looking at some HTTP attributes. Headless Chromium is essentially a full browser engine, so it provides all the information a regular browser does, but headless mode had a few obvious signals in the past. With the old method, anti-bot systems would check for:

  • User Agent “Headless” Phrase: Chromium in headless mode used to include a phrase in the user agent (UA) string as HeadlessChrome/x.y.z. This would tell the server that the browser was headless. In newer versions, this phrase was removed and UA was made to look like regular Chrome.
  • Navigator Webdriver Flag: In automation environments, the navigator.webdriver property is set to true. By default, Headless Chrome sets this flag to indicate whether a bot is present or not. The new mode still comes with navigator.webdriver=true, but this flag can be easily disabled with a command line argument.
  • Plugins and MIME Types: The navigator.plugins and navigator.mimeTypes arrays that return the browser’s installed plugins are empty in the old headless mode. However, in real user browsers, a few default plugins like PDF viewers are listed here. The new headless Chrome has closed this gap by filling in these lists just like the normal browser.
  • window.chrome Object: Chrome browsers have an object called window.chrome (with sub-objects like app, webstore, etc.). In the first headless versions, this object was not present and its absence was understandable. Now in the new mode, window.chrome is also present, so this difference is also fixed.
  • Graphics and WebGL Traces: Previously headless Chrome would return a virtual renderer called SwiftShader in WebGL queries because the GPU was not being used (e.g. Google Inc. (Google) SwiftShader), which was easily a sign of automation. The new mode returns the actual GPU information when possible; for example, WebGL UNMASKED_VENDOR_WEBGL now returns a realistic value like Google Inc. (Intel Inc.).

The above improvements thwart random bot-catching methods. However, headless mode browsers can still be caught by behavioral differences or more subtle fingerprint inconsistencies. For example, even if you set all of your browser’s values ​​to be realistic, behavioral analysis can still reveal that you are a bot, such as an automated script not behaving like a human (clicking very quickly, mouse movements that never make mistakes, etc.). So being completely invisible requires mimicking both fingerprint data and human behavior.

Next, we will cover the major browser API surfaces that fingerprints are created for and how they can be manipulated. We will then show how to define user profiles with JSON and load these values ​​into Headless Chromium using various methods. The differences between desktop and Android platforms will also be discussed.

Browser Fingerprinting Vectors and JS APIs

Browser Fingerprinting Vectors and JS APIs

The fingerprint data that websites collect includes a variety of information about the browser and the device. Below is a list of the main fingerprint vectors accessible via JavaScript and their sources:

  • User Agent: ​​The navigator.userAgent string carries information such as the browser name, version, and operating system. For example, for a desktop Chrome, we would see a value like “Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.5672.63 Safari/537.36“. In older versions, Headless Chrome would reveal itself by adding HeadlessChrome to this string. In fingerprint spoofing, the UA is usually modified to match the target profile (both browser and OS information must be consistent).
  • Navigator Object General Properties: under navigator there are several values ​​that can give away the identity of a browser:
    • navigator.platform: The platform/operating system the browser is running on. On desktop, this will take values ​​like “Win32“, “Linux x86_64“, “MacIntel“; on mobile Chrome, it will usually be “Linux armv8l” or blank. This value should also be set to match the profile.
    • navigator.hardwareConcurrency: Returns the number of logical processors (CPU cores) on the device. For desktops, this can be 4 or 8, and for mobile, it can be up to 8. This value is read from the system and can be detected with high accuracy. (As of Chrome 104, DevTools added a setting in the performance panel that allows simulating this number).
    • navigator.deviceMemory: It tells you the approximate RAM capacity of the device (in GB). For example, if it has 8 GB of RAM, it returns 8. This can also be included in the fingerprint.
    • navigator.language ve navigator.languages: Gives the browser’s default language and preferred language list ("en-EN" and ["en-EN","en"] ). These values ​​are also associated with the Accept-Language HTTP header and reveal the user’s regional settings.
    • navigator.webdriver: As we mentioned before, if the browser is under automation control (e.g. Selenium), it is usually true. For a normal user, this property is not present or is undefined. Therefore, in the realistic profile, it should be false (or the property is completely removed).
    • Others: navigator.doNotTrack (do not track setting), navigator.maxTouchPoints (touch screen points, usually 0 on desktop, >0 on mobile), navigator.appVersion, navigator.There are also values ​​such as vendor. Some of this information is also visible directly in userAgent and consistency is generally required.
  • Screen and Window Dimensions: Screen resolution and window dimensions can be obtained from the screen and window objects:screen.width, screen.height: User’s screen resolution (e.g. 1920×1080). screen.availWidth, screen.availHeight: Usable area excluding taskbar etc. screen.colorDepth: Color depth (usually 24 or 30 bits). window.innerWidth, window.innerHeight: Browser inner window size (visible area). window.outerWidth, window.outerHeight: Size including browser window frame (it often reported 0 in headless mode, that was a clue). devicePixelRatio: Device pixel ratio (e.g. retina displays like 2.0). Generally higher on mobile devices. These values ​​could be detected because they were usually fixed or abnormal (e.g. outerWidth=0) in headless mode. In fingerprint spoofing, the screen features of the desired device should be emulated.
  • Timezone and Date/Time – The user’s local time and timezone can be retrieved indirectly with JS:
    • Intl.DateTimeFormat().resolvedOptions().timeZone can be used to get the time zone reported by the browser’s OS.
    • With new Date().getTimezoneOffset() you can get the minute offset value according to UTC.
    • If these values ​​are not consistent with the IP address geolocation (different when using a proxy), it would be a red flag. Therefore, when spoofing, the time zone should be set to a profile that matches the chosen proxy/location.
    • It should also be consistent with navigator.language (e.g. if the proxy is USA, the language is English and the timezone is expected to be something like “America/New_York”).
  • Canvas Fingerprinting: The fingerprinting technique using Canvas API uses the browser’s graphic rendering differences. For example, some text and shapes are drawn on a canvas and the pixel data retrieved with CanvasRenderingContext2D.toDataURL() or getImageData() is hashed. Different browsers, operating systems and GPUs produce small pixel differences in this rendering, which creates a canvas fingerprint. By default, it is difficult to change this output consistently; however, anti-detect scanners add small randomizations to the canvas, producing a different but stable pseudo-hash for each profile. We will discuss this below.
  • WebGL Fingerprinting: Using the WebGL API, information about the device’s graphics hardware and driver can be obtained. In particular, if the WEBGL_debug_renderer_info extension is enabled in the browser, the gl.getParameter(ext.UNMASKED_VENDOR_WEBGL) and gl.getParameter(ext.UNMASKED_RENDERER_WEBGL) calls return the name of the graphics card manufacturer and renderer. For example, a real Chrome browser might return a vendor value like “Google Inc. (NVIDIA Corporation)” and a renderer value like “ANGLE (NVIDIA, NVIDIA GeForce GTX 1050 Ti Direct3D11 vs_5_0 ps_5_0)”. If Headless Chrome does not use GPU, it specifies a fake software renderer like “Google Inc. (Google)” and “ANGLE (Google, SwiftShader…”. To protect against fingerprinting, these values ​​are replaced with another realistic hardware. For example, it is possible to specify NVIDIA card information instead of Intel. Another method related to WebGL is to draw 3D scenes and hash the resulting image (like canvas). Anti-detection measures can also manipulate WebGL output, giving consistent but different results per profile.
  • Font and SVG: The list of installed fonts can also be a fingerprint. Web pages often try various font families with CSS and use Canvas or measurement techniques to figure out if they are installed on the device. This is also discriminatory, as the installed font set on each computer is different. Some anti-fingerprint browsers limit this list to a fixed subset or emulate a random font set for each profile. Similarly, SVG rendering can vary slightly from device to device.
  • Media Devices and Other APIs: navigator.mediaDevices.enumerateDevices() can be used to find out the number of connected cameras, microphones, and speakers. A real laptop usually has at least one microphone and camera; in a headless environment, sometimes none or only one dummy. Some browsers can also report the battery level with the Battery API (although this API has been largely deprecated in modern browsers). DeviceOrientation or Gyroscope APIs can tell the difference between mobile and desktop with information such as touchscreen (maxTouchPoints). All of this sensor data is also part of the fingerprint.

The above vectors form a fingerprint of the browser profile. An anti-bot system compares these values ​​of the incoming client with known realistic combinations. For example, if the userAgent shows Windows 10 Chrome but the platform is “MacIntel“, this inconsistency can be detected. Or if the proxy IP shows France but the browser language is Russian and the time zone is Chinese, this is a cause for alarm. Therefore, when spoofing a fingerprint, all these values ​​​​must be set as a harmonious whole. Below, we will discuss where these values ​​​​come from in the Headless Chromium environment and how we can override them.

Generating Fingerprint Values ​​and Override Points in Chromium

The Chrome/Chromium browser generates the above mentioned features in different layers. Some are fixed directly on the C++ side, in the content layer of the browser; some are determined by initialization parameters or system interfaces:

  • Creating User Agent: Chromium usually creates the user agent by combining compile-time constants and run-time platform information. In older versions, headless mode added HeadlessChrome to the UA string in ContentClient. In newer versions, this was done to make headless detection harder, so headless mode looks like a normal Chrome to the internet. We can still override this manually (we will get to the methods below).
  • Language and Locale: The browser language is determined by the –lang launch parameter or by the OS language. navigator.language and navigator.languages ​​come from this setting. The Accept-Language HTTP header also reflects this language in page requests. Locale also affects Intl outputs and number/date formats. To change these values ​​at the C++ level, it is sufficient to start the browser in that language, but the DevTools protocol can be used for dynamic changes (locale can be temporarily changed with Chrome DevTools Emulation.setLocaleOverride).
  • Timezone Source: Chrome uses the system timezone setting. It gets this from the global OS setting or from the Java-side TimeZone information on Android. Normally we can’t change it individually at the C++ level, but we can impose a different timezone on the browser via the DevTools protocol with Emulation.setTimezoneOverride. This allows the JS Date and Intl APIs to report a different timezone.
  • Core Count & Memory: navigator.hardwareConcurrency typically gets the number of CPU cores of the system with a call like std::thread::hardware_concurrency() . As of Chrome 117, DevTools can override this number for spoofing purposes with Emulation.setHardwareConcurrencyOverride . In fact, Chrome 104 announced that a setting for this value was added to the performance panel. At the C++ level, this value is not user controllable (it is fixed in the Chromium code), but it is possible to override it with devtools or JS after the browser is up and running. Similarly, navigator.deviceMemory is determined by getting the total RAM from the OS (rounded up to certain thresholds).
  • GPU and WebGL Information: Chrome fills in the WebGL features with information from the GPU drivers. In headless mode, if the real GPU is available, the information comes normally; if the GPU is disabled, SwiftShader is activated and the Google SwiftShader renderer information is returned. This part is processed in the ANGLE library responsible for graphics on the C++ side. Overriding is difficult because the web page directly reaches the hardware via the WebGL API. However, it is possible to catch these calls in the JS layer and return fake values ​​(we will give an example below). The advanced method is to manipulate the return of these functions when you fork Chromium; for example, the Camoufox project implements such patches for Firefox at the C++ level.
  • Canvas and Graphics Outputs: Canvas drawings rely on the browser’s graphics infrastructure (Skia etc.). It is not possible to output a fixed canvas for each profile at the C++ level, but anti-detect browsers do tricks like painting the entire canvas output black or adding small random noises to reduce entropy. This is usually done at the JS level by overriding the canvas APIs (like playing with the pixels after each drawImage). This will be covered in the injection section.
  • Navigator Object Construction: Chromium’s Blink engine creates the navigator object when the page is loaded and assigns its properties. Some come with constant strings, some come with system calls. For example, on the desktop, it sets the navigator.platform value to “Win32” etc. depending on the target OS during compilation. On the Android WebView, this is probably fixed to “Linux arm”. You can change these values ​​permanently with a C++ code change (for example, in a custom Chromium build, you can set the platform value to “iPhone”). On the other hand, it is possible to redefine them with JS while the browser is running.

In summary, some of the Chrome/Chromium fingerprint data comes directly from the system or from the build settings, while others can be set with parameters while running. Google has auto-fixed many details in the new headless mode to make it harder to detect bots; for example, the plugin list, the window.chrome object, etc. now look realistic without having to manually override them. This is actually a positive for bot developers, because patching every detail manually would lead to inconsistencies and increase the risk of getting caught. Still, advanced anti-bot systems can still catch small inconsistencies or side effects left by automation. For example, a fake navigator.plugins application, no matter how much it mimics PluginArray, can be caught by the memory structure or hidden features added. Therefore, the safest thing is to let the browser give realistic information in its raw form and carefully override only the necessary parts.

Now let’s move on to fingerprint spoofing methods and their applications.

Methods of Spoofing Fingerprint Values

When using Headless Chromium, there are several ways to fake fingerprint values ​​for a user profile. These can also be used together depending on the usage scenario.

Methods of Spoofing Fingerprint Values

The main methods are:

1. Spoofing with Command Line Arguments

Chromium-based browsers can change some of their features with certain flags during startup. Bot developers often use the following arguments:

  • User Agent Override: The browser’s UA string can be easily changed with the --user-agent="MOZILLA/5.0 …” argument. This affects both the User-Agent header in HTTP requests and the navigator.userAgent value. For example: A call like: chromium --headless --user-agent="Mozilla/5.0 (X11; Linux x86_64)…Chrome/100.0.4896.127 Safari/537.36” will start the browser with the specified UA.
  • Language and Region: A parameter like –lang=de-DE will start the browser with the German locale. This affects navigator.language and the Accept-Language header. Additionally, some bot platforms change the LANG environment variable or the regional format settings on Windows according to the profile.
  • Window Size and DPI: Even in headless mode, the browser can be given a virtual screen size. The –window-size=1366,768 argument can affect the values ​​of screen.width/height and window.innerWidth/innerHeight. There is also no direct argument for DPR (Device Pixel Ratio) in headless, but a flag like --force-device-scale-factor=1.5 can be used to affect the CSS pixel ratio.
  • Disable Automation Flags: Chrome internally enables some blink features when opened with automation. The --disable-blink-features=AutomationControlled argument sets the navigator.webdriver flag to false and hides some custom-defined functions. This removes the most obvious sign that the browser is under automation. This flag is also actively used in Puppeteer’s stealth extension.
  • WebRTC and IP Leaks: If WebRTC is used, –disable-webrtc or specific policy arguments can be used to prevent leaking of the local IP address. For example, --force-webrtc-ip-handling-policy=default_public_interface_only will only use the public IP, and will not make the local (LAN) IP accessible from JS. Alternatively, there are flags such as --webrtc-local-ip-address-ignore. This will prevent fingerprint methods that try to capture the local IP over RTCPeerConnection.
  • GPU and Graphics: Headless mode can disable GPU usage by default. If you want to turn on the GPU to resemble a real profile, you may need to enable GPU usage with parameters such as --disable-gpu=false or --use-gl=egl. However, this may not always be possible in a server environment. However, since many graphical values ​​​​come correctly in the new headless mode (even without the GPU), these arguments are not needed as much as before.

Command line arguments are fairly easy to implement and allow you to set basic properties before the browser is started. However, they do not meet every need; for example, there are no direct arguments for values ​​such as hardwareConcurrency or platform. Also, if you want to emulate a mobile profile (such as making navigator.platform “Linux armv8l“), you cannot do this with a flag. In these cases, you have to resort to other methods.

2. Emulation and Override with DevTools Protocol

Chrome’s DevTools (CDP – Chrome DevTools Protocol) interface provides a powerful API that allows emulating various features within the browser. Automation libraries (Puppeteer, Playwright, Selenium+CDP) can perform a number of overrides using this protocol. Important CDP commands are:

  • UserAgent and Accept-Language Override: The Network.setUserAgentOverride command overrides the UA and optionally Accept-Language to be used when requesting a page in the running browser. This allows for dynamic replacement of the command line argument. For example, in Puppeteer, page.setUserAgent(spoofedUA) uses this CDP command.
  • Screen and Device Metrics Emulation: With Emulation.setDeviceMetricsOverride, values ​​such as screen resolution, viewport size, device scale factor can be set. This method is also used in mobile device emulation. Among the parameters of this command, hardwareConcurrency is now included. In other words, a wide profile (width, height, DPR, touch support, number of CPU cores, etc.) can be set with a single command.
  • Timezone Override: As mentioned above, the browser’s local time zone can be displayed differently with Emulation.setTimezoneOverride. For example, by setting the value “Europe/London“, the output of Intl.DateTimeFormat().resolvedOptions().timeZone can be changed to London.
  • Locale & Languages Override: The Emulation.setLocaleOverride command changes the browser’s default locale. This affects the navigator.language and navigator.languages ​​arrays. For example, you can set navigator.language to French by giving “fr-FR“. (Note that this command is experimental, but tools like Playwright can use it at a low level.)
  • Navigator.hardwareConcurrency Override: A new feature added to CDP is the Emulation.setHardwareConcurrencyOverride command. Although this command appears to be experimental, if supported, the navigator.hardwareConcurrency in the browser will be set directly to the integer you specify. For example, it can be used to set it to 8.
  • Geolocation Override: For location-based controls, you can spoof Geolocation API results by providing latitude, longitude, and precision with Emulation.setGeolocationOverride.
  • Sensor (Disable): Touch events can be triggered with commands like Emulation.setEmitTouchEventsForMouse etc., and a touch screen can be acted upon with Emulation.setTouchEmulationEnabled. This affects the navigator.maxTouchPoints value (and therefore whether it is mobile or not).

The advantage of using the DevTools protocol is that it provides an emulation layer without having to recompile the browser. For example, when creating a context in Playwright, settings such as userAgent, locale, colorScheme, timezone can be given directly and the page on the internet is opened with these settings. These methods are preferred because they are more flexible than command line arguments and can be changed at runtime.

But we can’t do everything with CDP. For example, there is no direct CDP command for navigator.platform or navigator.deviceMemory. This is where JavaScript override comes into play.

3. Feature Injection with JavaScript

The most common and powerful way to change values ​​in the browser is to inject a special JavaScript code (evaluateOnNewDocument or similar) just before the page loads, to change global variables and functions. This method is also the basis for tools like the Puppeteer Stealth plugin. The strategy is to run before the page itself, exposing the APIs that the page will see and use, as we define them.

A few important JS override examples:

  • Navigator Property Override: To mock simple properties, Object.defineProperty is used. For example: // This code should be run before the page is opened (addInitScript / evaluateOnNewDocument) Object.defineProperty(navigator, 'platform', { get: () => 'Win32' }); Object.defineProperty(navigator, 'language', { get: () => 'en-US' }); Object.defineProperty(navigator, 'languages', { get: () => ['en-US', 'en'] }); Object.defineProperty(navigator, 'hardwareConcurrency', { get: () => 4 }); Object.defineProperty(navigator, 'deviceMemory', { get: () => 8 }); This way, when page scripts read navigator.platform, they see exactly what we gave them. It is important to note that many of these properties are read-only and cannot normally be changed from JS. However, it is possible to redefine them as configurable with Object.defineProperty. Stealth plugin developers are careful to preserve the original object structure when doing such overrides (for example, making the type PluginArray for navigator.plugins). We should also keep the data types and property attributes similar to the original when overriding.
  • navigator.plugins ve navigator.mimeTypes: These two are hard to fake but important areas. If you don’t touch anything, headless Chrome gives empty lists in older versions, which is suspicious. The Stealth plugin implements a very creative solution here: faking the plugin objects to be displayed in JavaScript. For example, they create an object representing the PDF Viewer plugin and put it in the navigator.plugins array. While doing this, they go into details such as setting the prototype of the array as PluginArray, filling in properties such as length and entries correctly. In this way, a site that does a simple check sees navigator.plugins.length>0 and thinks it’s normal. However, advanced detectors can understand whether the functions of these plugin objects are defined in native code or added later. In fact, DataDome researchers have noted that the plugin objects added by Puppeteer Stealth differ slightly from those in real Chrome. Still, most basic checks fall for these JS overrides.
  • Canvas API Override: To prevent or fix the canvas fingerprint, the canvas methods are targeted. For example: const origToDataURL = HTMLCanvasElement.prototype.toDataURL; HTMLCanvasElement.prototype.toDataURL = function(…args) { const data = origToDataURL.apply(this, args); // Example: Get the data and hash it, replace it with a known constant hash or manipulate the pixels. // Simple approach: Add a small random change on each call (different fingerprint each time). return data; }; More sophisticatedly, some “noise” can be added to the canvas during the drawing process. For example, if you change the color of each pixel slightly, the human eye won’t notice it, but the hash will change. Projects like Camoufox implement techniques that distort the canvas and WebGL output in a profile-specific but constant way.
  • WebGL API Override: A common technique is to patch WebGL’s getParameter function. In particular, when reading debug renderer info, fake information is returned: const getParameter = WebGLRenderingContext.prototype.getParameter; WebGLRenderingContext.prototype.getParameter = function(param) { // Code value of UNMASKED_VENDOR_WEBGL and UNMASKED_RENDERER_WEBGL constants: if(param === 0x9245 /* UNMASKED_VENDOR_WEBGL /) { return "Intel Inc."; // For example, fake as Intel } ​​if(param === 0x9246 / UNMASKED_RENDERER_WEBGL */) { return "Intel(R) Iris(TM) Plus Graphics 640"; // Fake integrated GPU name } return getParameter.call(this, param); }; This code will give the value we specify instead of the actual value when the page tries to get GPU information via WebGL. The same tactic can be applied to functions like CanvasRenderingContext2D.measureText (to prevent font metric differences). Some tools even override AudioContext APIs and break the audio fingerprint.
  • Other Libraries and Objects: There are also minor points such as webkit* features that show the trace of automation in the browser, Permissions API (usually permissions in automation can be different granted/denied by default). Stealth scripts usually contain many minor patches, as can be seen on GitHub. For example, some vars are removed from Chromium DevTools, which is known to contain webdrivers, or the MediaCapabilities API is added, which is in the real browser but not in headless, etc.

JavaScript injection method is quite flexible and powerful, but it also carries the risk of getting caught if it is implemented incorrectly. Because every browser has a natural behavior, prototype chain structure. If a site finds an inconsistency in this structure, it can detect the fraud. For example, if the navigator.plugins array should have the PluginArray prototype but has the normal Array prototype, this is abnormal. Therefore, it is necessary to be very careful when overriding at this level. When examining the code of the Puppeteer Extra Stealth plugin, it is seen that these details are meticulously taken care of – the plugin developers even created a fake PluginArray type object and filled it with PDF plugin information copied from the real Chrome.

Note: One disadvantage of overriding with JS is that anti-fingerprint scripts running in the browser may notice this. For example, some websites check the string form of JS functions (Function.toString()) to see if [native code] is written in them. If the functions we override fail this check (i.e. are later determined), we may be caught. Advanced forgery libraries will also try to imitate the original native code string in this case so that it is not detected.

4. Using Extensions

Headless mode normally disables extensions, but if desired, it is possible to load a Chrome extension with the –load-extension parameter and run it in the background (the content scripts of extensions usually work even in headless mode). Some security tools used to use plugin-based approaches. For example, extensions like Privacy Pass or uBlock can also run in headless mode. In our case, there are some anti-detect extensions developed for fingerprint spoofing (CanvasBlocker for Firefox, webrtc leak prevent for Chrome, etc.).

However, the extension-based solution remains vulnerable to modern fingerprint detection. The experience of the Multilogin team shows that simple plugin injections are now easily detected. Some sites can detect the presence of plugins through Content Security Policy, DOM analysis, etc. Therefore, current approaches tend to modify the browser directly (more on that in the next article). However, in limited scenarios, it may be possible to use an extension to modify specific headers (e.g. User-Agent or Referer), disable WebRTC, or inject additional scripts.

If you already have a browser extension, you can use headless Chromium to:

  • To ensure that it is loaded with the --disable-extensions-except=/path/to/extension and --load-extension=/path/to/extension arguments.
  • Scripts can be injected into the page via the extension’s content scripts or devtools commands can be sent via the chrome.debugger API.

This method may make sense, especially when automating existing browsers (e.g. regular Chrome, Edge) rather than a built-in Chromium under our control. However, it is a more laborious and limited method.

5. Source Code Modification (Chromium Patching)

The most radical solution is to remove fingerprints by modifying the browser engine itself. This approach is used in most special browsers called anti-detect browsers. Thanks to this, no site, including Google, can see the real fingerprint, they all see only the spoofed values.

Here are some examples of changes that can be made at the source code level:

  • Change the browser’s user agent creation function and return a fixed or configurable value.
  • Patching the places that generate properties in the Navigator interface (Blink’s IDL definitions and implementations), for example returning a constant value in the NavigatorHardwareConcurrency class.
  • Injected code into graphics libraries for Canvas and WebGL output that added minor noise (the Mimic browser made changes that broke AudioContext and WebGL output).
  • Emulate a random font list every time a new profile is created (Mimic is perhaps the only browser that can create a virtual font list for each profile, thus completely defeating font fingerprinting).
  • There are closed source parts of Chromium (Google API keys, Safe Browsing etc.), but the fingerprint related parts are generally open source and can be changed on Blink and V8 side.

Of course, this method requires a lot of technical knowledge and effort. Compiling Chrome code, maintaining the changes made, and adapting it to each new version is a serious job. That’s why some teams develop special browsers based on Firefox, because Firefox can be modified more easily and is comfortable in terms of license/compatibility (as in the Camoufox example, Firefox was preferred). However, the disadvantage is that since many anti-bot solutions are Chromium-focused, the number of those who do automation with Firefox is low; this alone can make Firefox traffic suspicious.

In short, if complete control and 100% consistency are desired, the best solution is at the source code level. However, since this is not possible for most users, in practice the launch arg + CDP + JS injection combinations described above are used. Many of us already work with tools such as Puppeteer and Playwright, and they also offer add-ons that include these methods.

User Profile Definition and JSON Example

Success in fingerprint spoofing is achieved by creating a consistent user profile. A profile represents the data of a real device + browser combination. Keeping this profile data in a format like JSON and applying it to the browser when needed makes it easier. For example, some commercial services (like FingerprintSwitcher) offer a database of fingerprints collected from 50,000+ real devices, and you can pull one and apply it to your browser.

Here is an example of a simple user profile JSON:

{
  "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36",
  "platform": "Win32",
  "hardwareConcurrency": 8,
  "deviceMemory": 16,
  "languages": ["en-US", "en"],
  "language": "en-US",
  "timezone": "America/New_York",
  "timezoneOffset": -240,
  "screen": {
    "width": 1920,
    "height": 1080,
    "availWidth": 1920,
    "availHeight": 1040,
    "innerWidth": 1920,
    "innerHeight": 969,
    "devicePixelRatio": 1
  },
  "gpu": {
    "vendor": "Google Inc. (NVIDIA Corporation)",
    "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 1060, Direct3D11)"
  },
  "webglParams": {
    "UNMASKED_VENDOR_WEBGL": "NVIDIA Corporation",
    "UNMASKED_RENDERER_WEBGL": "NVIDIA GeForce GTX 1060/PCIe/SSE2"
  },
  "webRTC": {
    "localIps": ["192.168.1.15"],
    "publicIp": "34.121.55.21"
  },
  "fonts": ["Arial", "Calibri", "Times New Roman", "Courier New"],
  "plugins": [
    {
      "name": "Chrome PDF Viewer",
      "filename": "internal-pdf-viewer",
      "description": "Portable Document Format"
    },
    {
      "name": "WebKit built-in PDF",
      "filename": "pdf",
      "description": "Portable Document Format"
    }
  ],
  "mediaDevices": {
    "videoInputs": 1,
    "audioInputs": 1,
    "audioOutputs": 1
  },
  "navigatorProperties": {
    "maxTouchPoints": 0,
    "deviceMemory": 16,
    "vendor": "Google Inc.",
    "webdriver": false,
    "doNotTrack": null
  }
}

The profile above contains values ​​that could belong to a desktop computer with an NVIDIA graphics card, running Windows 10 + Chrome browser, for example. It is important to note the logical consistency of this profile:

  • platform = “Win32” and userAgent contains “Windows NT 10.0; Win64; x64“. So the operating system is a consistent Windows 10, 64-bit.
  • GPU vendor is “Google Inc. (NVIDIA Corporation)”; this is in line with Chrome’s WebGL reporting format (Chrome usually adds the “Google Inc.” constant as the vendor string).
  • WebRTC local IP is a private IP (192.168.x.x), public IP is an example with a US location. Timezone is “America/New_York” and language is “en-US” to ensure geographic compatibility.
  • The hardware specs (8 CPUs, 16GB RAM) are reasonable for a modern desktop.
  • The screen size is 1920×1080, DPI 1, which is compatible with desktop monitor.
  • The plugins list includes Chrome’s built-in PDF viewer and a similar PDF plugin (in this example there are two plugins listed, in reality the new version of Chrome may only show one but here we have two similar entries for variety).
  • Media devices are 1 each (probably 1 microphone, 1 speaker, 1 webcam).

When launching a browser using this JSON profile:

  • UA, language, window-size are set via command line.
  • Timezone, locale, hardwareConcurrency override is done with DevTools.
  • With JS injection, platform, deviceMemory, plugins, canvas, webgl etc. are set.
Browser Fingerprinting Vectors and JS APIs

In real life, instead of doing these operations manually, ready-made libraries are used. For example, the Fingerprint Switcher service returns similar JSON and a plugin like playwright-with-fingerprints that uses it takes this JSON and applies it to the browser. In this way, fingerprint manipulation is automated.

In conclusion, there are open source tools that we can use to perform fingerprint spoofing in Headless Chromium, and they implement a number of measures on our behalf. However, no matter which tool we use, the important thing to understand is the following principles:

  • Choose or create a real profile and apply all of its values ​​consistently.
  • If possible, override via in-browser APIs (CDP, injection), as this is more flexible.
  • Try not to leave any inconsistencies (IP-Language-Time compatibility, UA-platform compatibility, etc.).
  • Not only static fingerprint, but also dynamic fingerprint (behavioral tracks) are important: Browse like a human, add random small delays, simulate cursor movement etc., otherwise even the most perfect fake browser can be caught by behavior.

Differences Between Desktop and Android

Differences Between Desktop and Android Browser Fingerprinting Vectors

Now let’s open a separate parenthesis for the Android platform. Many of the things we explained for the desktop also apply to Android Chrome/WebView, but there are some differences:

1. Headless Mode Support: The official Chrome app cannot be run in headless mode on Android. Headless is generally available on desktop Linux/Mac/Windows. On the Android side, if we want to use a browser without an interface, the typical approach is to embed the Android WebView in an application and run it invisibly (off-screen). For example, in an Android application, a page can be loaded without adding the WebView to the interface and the DOM can be drawn in the background. Indeed, some libraries offer a HeadlessInAppWebView component in environments such as Flutter (to run the WebView in the background). However, accessing the Chrome DevTools protocol may be more difficult in this method (it may be necessary to open the debug mode of the WebView and connect it to the PC).

2. JNI and System Integration: Android WebView (and Chrome) gets most of its information from the Java layer.

For example:

  • navigator.platform in Android WebView usually returns “Linux armv7l” or a similar string, this is probably a constant set according to the WebView’s Build.MODEL/CPU information.
  • navigator.hardwareConcurrency returns a value according to the CPU big.LITTLE structure, such as 4 or 8 on mobile. This value may be retrieved directly with Runtime.getRuntime().availableProcessors() and transferred to Blink via JNI.
  • navigator.plugins is probably empty on mobile Chrome or it could just be an entry for the PDF viewer. Because there is no Pepper Plugin support on mobile. In fact, the Chrome team has thought about fixing navigator.plugins and mimeTypes even on the desktop since Flash was finished. This API probably always returns empty on mobile. Since this is a normal situation, maybe mobile browsers will not be suspicious because the plugin list is empty.
  • Timezone and Language: On mobile, the user language and timezone settings come from the device settings. On an Android device, it is not possible to change these on an app-by-app basis (without root). So the WebView will get whatever language-region the phone is in. The timezone is the device’s system timezone. On the desktop, we could override these, but on Android, you might need to patch the Date and Intl functions into the page as a workaround. This means even more advanced JS injection (and can be error-prone).
  • Differences between Canvas/WebGL: Mobile GPUs produce different renderer strings than desktops. Also, Canvas drawings may use different algorithms. If you are going to emulate a profile on mobile, WebGL vendors should be emulated such as WebKit (for Samsung devices) or Qualcomm etc. chipset names. While there are generally ANGLE (DirectX/OpenGL) strings on desktops, OpenGL ES/Vulkan related strings appear on mobiles. So, this detail should be taken into consideration when faking.

3. Performance and Behavioral Differences: Mobile devices may be limited in some APIs due to hardware limitations:

  • MediaDevices.enumerateDevices() usually gives only one front and one rear camera on a phone. If there is no webcame on the desktop, it may return 0. This aspect of the profile should also be considered.
  • Mobile browsers always have touch support (maxTouchPoints >= 1). Desktop doesn’t. So if you want a mobile fingerprint, maxTouchPoints should definitely be >0 and maybe emulate events like ontouchstart.
  • Android WebView, some APIs may be restricted (WebRTC for example, subject to application permissions, does not silently obtain an IP). But Chrome Android can probably leak WebRTC IP. To turn this off on mobile, you may need to look at the “WebRTC local IP” option in Chrome settings, or it may only be blocked with a VPN.
  • The font list on Android is very limited (system fonts are a certain set). There can be hundreds of fonts on the desktop. This means that since font enumeration attempts on a mobile fingerprint will give similar results on every device, perhaps mobile bots are less risky in this respect. But on a desktop fingerprint, fonts are quite distinctive.

4. App-Based vs Browser-Based Automation: On the Android side, browser automation is mostly done with tools like Appium, by opening the real browser and checking it. In this case, since we do not control Chrome directly, our chance of overriding with CDP decreases (it is possible to enable Chrome mobile remote debug protocol, but it may not give access to everything). If you open a Chrome browser with Appium, navigator.webdriver will automatically be true (Chrome probably detects this). Turning it off is a whole other issue (maybe not). So for Android, maybe the most logical thing is to write a custom automation app that uses invisible WebView instead of the browser, or rooting the device and modifying the browser apk may require extreme solutions.

5. Security and Permissions: What an Android application can do is more limited compared to a desktop process. For example, on the desktop, we have the file access we want, network control, etc. The Android application is in a sandbox. This may not be so critical for the fingerprint, but for example, adding a mitmproxy certificate to Android WebView, changing the TLS fingerprint, etc. is more difficult.

In short, fingerprint spoofing on Android can be more challenging:

  • If it is WebView based, injection is still possible (we can inject our own code with JavascriptInterface or evaluateJavascript).
  • But if it’s Chrome app based, maybe it won’t be possible at all (because Chrome app updates itself, there are closed source parts, etc.).
  • On Android, it was possible to hijack system calls using an environment like Xposed to lie to the browser (e.g. spoofing Build.MODEL etc., or overriding the getTimeZone function), but this is too technical for the average user.

In the real world, many bot operations run headless browsers on server environments rather than on Android devices. Because managing the Android browser can be cumbersome and slow. However, if a mobile browser view is required, desktop Chrome’s mobile emulation feature is used (i.e., mobile UA is given as –user-agent, and the screen size and touch event are set with Emulation.setDeviceMetricsOverride, so the site pretends to be mobile). This approach allows you to fake an “Android fingerprint” by making a PC browser look like a mobile device.

In summary, here are the main differences between desktop and Android:

  • There is no headless direct on Android, there is WebView.
  • Some fingerprint areas are fixed or limited on Android (plugins are always empty, font list is fixed, timezone is hard to change).
  • Android fingerprint is generally less complicated (because the devices are similar) but changing it is also limited.
  • If full control over a real Android device is needed, a custom apk or root may be required (which may not be practical).

So in most scenarios, Chrome + mobile emulation + spoofing is used on the desktop to achieve mobile fingerprint. It’s kind of the best of both worlds: full control on the desktop but presenting itself to the website as a mobile device. This gets the mobile-friendly site behavior and bypasses mobile fingerprint checks.

Conclusion and Summary

Browser fingerprint spoofing is an increasingly sophisticated field. With Headless Chromium, it’s possible to impersonate both desktop and mobile users, but each fingerprint vector needs to be handled correctly. We’ve gone into the advanced technical details in this article:

  • We’ve seen Headless Chrome’s fingerprint differences from past to present and Google’s efforts to reduce them.
  • We discussed the fingerprint vectors (Navigator features, screen, language, canvas, WebGL, etc.) one by one and what to pay attention to for forgery.
  • We examined the sources of these values ​​in Chromium and where they can be overridden (C++ vs JS vs DevTools).
  • We learned that a combination of Launch arg, CDP Emulation and JS injection methods is often required. We showed how to override with code examples.
  • We defined all the values ​​together with the JSON profile format and emphasized that this is a regular way to implement it. We gave examples from real device fingerprints.
  • We explained the differences between Desktop and Android and additional restrictions in the Android environment. We stated that it would be easier to produce a mobile fingerprint with desktop emulation if possible.

Last advice: When you set up your own anti-detection system, be sure to test your browser. Open it with tools like CreepJS, FingerprintJS, browserleaks.com and see which areas still leak original values. A small detail (like WebGL antialiasing support) can give it away. Make sure you set all the parameters when loading a profile, or pull from a trusted fingerprint pool. And instead of making a big deal at once, add incrementally: First change UA, then timezone, then canvas, testing each step.

Remember, the ultimate goal is to give the impression of a “real human browser.” Achieving this requires technology and engineering, as well as a coherent storyline for the user you’re imitating (geography, language, device type, it all needs to be consistent). By following these principles, you can perform very successful spoofing with Headless Chromium and make it much easier to tell if you’re a bot.

Looking to implement advanced browser fingerprint spoofing in your headless automation system?
The reverseengineer.net team is ready to assist.
From C++ patching to WebView JNI hooking, we’ve already helped high-level clients across industries achieve full stealth mode. Reach out and let’s discuss how we can tailor a solution for you.

Let's Work Together

Need Professional Assistance with Reverse Engineering or Cybersecurity Solutions? Our Team is Ready To Help You Tackle Complex Technical Challenges.