Liinked Chromium v150

Complete Modification List

Every change made to the vanilla Chromium 150 source tree to produce a stealth, fingerprint-spoofable browser. 36 modifications across 9 functional areas.

Index

Jump to a modification

01

FingerprintConfig Subsystem (Core Infrastructure)

What it does

A per-profile configuration struct that holds all spoofable values (user agent, platform, screen size, canvas seed, audio seed, WebGL strings, timezone, etc.). The config is loaded from a fingerprint/ folder inside the profile directory on disk, cached in the browser process, then serialized into a Mojo IPC message and pushed to every renderer process before it initializes.

Why it is needed

All the individual spoofing hooks below need a single, authoritative source of truth that is: (a) per-profile so different tabs can run different identities, (b) available in both the browser process and the renderer process, and (c) installed before any JavaScript runs. Without this plumbing every surface would need its own out-of-band channel, making the system impossible to maintain.

02

navigator.webdriver Hard-Pinned to false

What it does

The webdriver getter on the Navigator object unconditionally returns false regardless of how the browser was launched or what CDP commands have been sent.

Why it is needed

Stock Chromium returns true when either --enable-automation is passed or when Playwright/Selenium fires the Emulation.setAutomationOverride CDP command, which they do on every new page. A JavaScript Object.getOwnPropertyDescriptor probe on Navigator.prototype.webdriver can detect any userland override script, so the only safe fix is at the C++ source before the property is ever exposed.

03

User-Agent String Spoofing

What it does

When the active FingerprintConfig contains a non-empty user_agent string, that string is returned by every code path that reads the UA: the browser-process GetUserAgent() (used for HTTP request headers and InitializeRenderer()), the renderer-side navigator.userAgent getter, and the equivalent getter in web workers so workers cannot disagree with the main frame.

Why it is needed

Detectors compare the User-Agent HTTP header, navigator.userAgent, and the UA-CH Sec-CH-UA-* headers. Any mismatch between these three is flagged as automation. Spoofing must cover all three surfaces consistently.

04

UA-CH Metadata Spoofing (navigator.userAgentData)

What it does

Rebuilds the full UserAgentMetadata struct to match the spoofed UA string: the brand/version list is regenerated using Chromium's real GenerateBrandVersionList function seeded with the spoofed major version number (so greased brand strings and shuffle order are authentic), platform is mapped from navigator.platform-style values to UA-CH format, platform_version / architecture / bitness / mobile are set consistently. navigator.userAgentData.platform and getHighEntropyValues() are patched in the renderer as well.

Why it is needed

Services like BrowserScan flag "different browser version" and "different operating systems" when the UA string and the Sec-CH-UA-* headers disagree. Using the real GenerateBrandVersionList means the Not/A)Brand greased entry matches what that Chrome version would actually produce, defeating detectors that validate the grease pattern.

05

navigator.platform Spoofing

What it does

Both the NavigatorBase::platform() (used by workers and shared execution contexts) and the main-frame Navigator::platform() check the active FingerprintConfig and return the configured platform string (e.g. "Win32", "MacIntel", "Linux x86_64") when one is set.

Why it is needed

navigator.platform is one of the oldest and most widely probed navigator properties. It must match the OS implied by the spoofed user-agent string and the UA-CH platform string, otherwise every multi-signal detector flags the inconsistency immediately.

06

navigator.languages and Accept-Language Header Alignment

What it does

When the FingerprintConfig specifies a languages string (e.g. "en-US,en"), navigator.languages is populated from that string instead of the OS preference. ComputeAcceptLanguage() is intercepted in the browser process so the HTTP Accept-Language header is regenerated from the same value.

Why it is needed

Server-side detectors compare the Accept-Language header on the network request against navigator.languages in the page's JavaScript. A mismatch between the header (generated in the browser process) and the JS property (generated in the renderer process) is a reliable bot signal that cannot be fixed with a userland script alone.

07

navigator.hardwareConcurrency Spoofing

What it does

Returns the hardware_concurrency value from the FingerprintConfig instead of the actual number of CPU cores on the host machine.

Why it is needed

A headless server typically has many cores (16, 32, 64). Real desktop Chrome running on consumer hardware reports 4, 8, or 12. An unexpectedly high CPU count is a strong automation/server signal for fingerprinting services.

08

navigator.deviceMemory Spoofing

What it does

Returns the configured device_memory value, snapped to the nearest value in the spec-mandated set {0.25, 0.5, 1, 2, 4, 8} GiB before exposing it to JS.

Why it is needed

The Device Memory spec only permits a small set of values. Returning anything outside that set (e.g. 16 or 128) is itself a bot detection signal. Snapping ensures the spoofed value is always spec-compliant.

09

Screen and Window Dimension Spoofing

What it does

All screen and window geometry properties return values from the FingerprintConfig when set. outerHeight and outerWidth are computed as availHeight/availWidth (i.e. "maximized window") because real Chrome on a maximized window returns those values. screenX and screenY are pinned to 0 to prevent leaking the physical monitor layout of the operator's machine.

Why it is needed

Detectors reconstruct screen.height - taskbarHeight == availHeight == outerHeight and flag any internal inconsistency. Leaking real monitor geometry (e.g. X=1920 on a dual-monitor setup) also immediately identifies the operator's hardware. All five properties must be internally consistent with each other and with the spoofed prefers-color-scheme / color-depth.

10

Timezone Spoofing

What it does

Installs a process-wide timezone override via Blink's TimeZoneController, re-notifying V8 and all worker isolates so that Date, Intl.DateTimeFormat, Intl.DateTimeFormat().resolvedOptions().timeZone, and all Intl APIs reflect the spoofed timezone immediately. Applied in RenderThreadImpl::SetFingerprintConfig().

Why it is needed

Timezone is cross-checked in two ways: (1) Intl.DateTimeFormat().resolvedOptions().timeZone must match the browser's reported locale, and (2) it must match the geolocation of the exit IP the proxy uses. Using TimeZoneController is the only way to override the timezone in both the main isolate and all workers simultaneously.

11

CSS Media Query Spoofing

What it does

The following CSS media features check the FingerprintConfig and return the configured value before falling through to real platform APIs: prefers-color-scheme (dark / light), prefers-contrast (more / less / custom / no-preference), prefers-reduced-transparency (reduce / no-preference), inverted-colors (inverted / none), forced-colors (active / none), device-width / device-height (from screen_width / screen_height), color bits per component (from color_depth).

Why it is needed

Fingerprinting services probe media queries both via CSS and via JavaScript's window.matchMedia(). The results must be consistent with the spoofed screen dimensions, platform, and operating system. On a headless Linux server the defaults (no dark mode, no forced colors, no reduced transparency) differ from a typical Windows desktop profile, making the discrepancy detectable.

12

Canvas Fingerprint Farbling

What it does

A deterministic ±1-LSB-per-channel delta is applied to every RGB pixel of canvas read-backs. The delta is keyed on a composite seed derived from canvas_seed in the FingerprintConfig XOR-mixed with a hash of the page's security origin. The exact same CanvasFarbleDelta(seed, x, y, channel) helper is used on both the toDataURL() / toBlob() encoding path and the getImageData() path, and both use absolute canvas coordinates so the values are byte-identical regardless of what rectangle the caller reads.

Why it is needed

Canvas fingerprinting is one of the two most reliable browser fingerprinting techniques. Every profile must produce a unique, stable hash. Using absolute coordinates prevents the "call getImageData and toDataURL and diff them" Canvas Tampering detection used by CreepJS and BrowserScan.

13

getBoundingClientRect() and getClientRects() Farbling

What it does

A sub-pixel deterministic jitter (keyed on canvas_seed and element identity via a pointer hash XOR with a prime) is added to the x, y, width, and height of every DOMRect returned by getBoundingClientRect() and getClientRects(). The same element always returns the same jittered values within a session.

Why it is needed

Fingerprinting services render specific characters in specific fonts, then call getBoundingClientRect() on the element. The exact floating-point sub-pixel values are hashed. This farbling makes those hashes unique per profile while being invisible to the user (delta is sub-pixel) and passing the "call twice and diff" stability check.

14

measureText Farbling

What it does

Applies a deterministic sub-pixel delta (less than 1e-3 px) to all TextMetrics values returned by CanvasRenderingContext2D.measureText(): width, actualBoundingBoxLeft, actualBoundingBoxRight, fontBoundingBoxAscent, fontBoundingBoxDescent, actualBoundingBoxAscent, actualBoundingBoxDescent. The delta is keyed on a hash of the text string and font family name.

Why it is needed

FingerprintJS, CreepJS, and similar services fingerprint browsers by rendering a known string in a list of known fonts and hashing the resulting width values. The sub-pixel jitter makes the hash unique per profile without producing visibly wrong text layout.

15

Audio Fingerprint Farbling

What it does

A deterministic noise amplitude (keyed on audio_seed) is added to audio sample data at multiple points in the Web Audio pipeline: the offline render quantum before it is written to the output AudioBuffer, the realtime destination render quantum, the AudioBuffer constructor when loaded from an AudioBus, and the AnalyserNode FFT and time-domain output arrays.

Why it is needed

Audio fingerprinting (the "AudioContext hash" technique) creates an OfflineAudioContext, renders a specific signal through an OscillatorNode and DynamicsCompressor, and hashes the resulting float array. This is the second most reliable browser fingerprinting technique alongside canvas. Per-profile noise makes the hash unique without affecting audible playback (amplitudes are ~1e-4 or less).

16

WebGL Vendor / Renderer Spoofing

What it does

WEBGL_debug_renderer_info extension is always reported as supported (hiding it is itself a detection signal). UNMASKED_VENDOR_WEBGL returns webgl_vendor from the FingerprintConfig. UNMASKED_RENDERER_WEBGL either returns webgl_renderer directly, or when webgl_renderer_mode = "strip" is set, calls the real GPU string and strips device-unique identifiers (PCI device IDs, model-specific suffixes, exact VRAM size, shader model strings) while preserving the GPU family/vendor/series. In both cases the real GL_RENDERER / GL_VENDOR IPC is executed first and its result discarded, so the GPU driver call timing is paid and timing probes do not detect a shortcut.

Why it is needed

The raw GPU renderer string is unique to a specific card revision and driver version. Returning it directly fingerprints the operator's hardware. Hiding the extension entirely is flagged by pixelscan and fingerprint.com as masking behavior. Spoofing the values without paying the driver call cost is caught by BrowserScan's "WebGL exception" throughput probe.

17

WebGL Extension List Filtering and Reordering

What it does

When the FingerprintConfig contains a webgl_extensions allowlist, getSupportedExtensions() returns only extensions present in that list, in the order defined by the list (intersected with what is actually enabled). The same filter is applied inside ExtensionSupportedAndAllowed() so getExtension() and getSupportedExtensions() cannot disagree.

Why it is needed

The WebGL extension list is hashed by CreepJS and BrowserScan. The list varies by GPU vendor/driver and is a strong fingerprint. Returning a different list from getSupportedExtensions() vs. getExtension() is itself a tampering signal, so both paths must go through the same filter.

18

WebGL Parameter Overrides

What it does

A JSON dictionary stored in webgl_parameters_json in the FingerprintConfig maps WebGL enum names to override values. The GetFloatParameter(), GetIntParameter(), GetInt64Parameter(), GetUnsignedIntParameter(), GetWebGLFloatArrayParameter(), and GetWebGLIntArrayParameter() paths all consult a LookupWebGLParameterOverride() helper and substitute the configured value when present.

Why it is needed

GPU capability parameters (max texture size, max viewport dimensions, max vertex attributes, etc.) vary by GPU and driver and are routinely hashed by fingerprinting services. Overriding them allows a profile to match a specific target GPU's parameter set exactly.

19

WebGL ReadPixels Noise

What it does

After gl.readPixels() completes for the (GL_RGBA, GL_UNSIGNED_BYTE) format, the same CanvasFarbleDelta() helper used by the 2D canvas path is applied to the resulting pixel buffer using absolute viewport coordinates.

Why it is needed

Some fingerprinting services use gl.readPixels() instead of (or in addition to) canvas.toDataURL() to extract the WebGL canvas hash. The same farbling ensures the resulting bytes match what toDataURL() would return for the same pixels.

20

WebGPU Disable Mode

What it does

When disable_webgpu is set in the FingerprintConfig, navigator.gpu.requestAdapter() resolves its promise with null immediately, without contacting the Dawn GPU backend. navigator.gpu remains present (as in real Chrome).

Why it is needed

The WebGPU adapter exposes GPU vendor, architecture, driver version, and a list of supported features that are highly device-specific. Blocking the adapter prevents leaking this information while keeping the API surface intact (its absence would itself be a signal).

21

WebRTC IP Leak Prevention

What it does

Four layers of WebRTC protection: (1) Forces kDisableNonProxiedUdp IP handling policy and mDNS obfuscation when any FingerprintConfig is active, preventing STUN from revealing the real exit IP. (2) In proxy_only mode, host candidates are suppressed before they reach the onicecandidate event. (3) In hybrid mode with webrtc_public_ip set, srflx candidate addresses are rewritten to the configured exit IP. (4) In hybrid mode, if ICE gathering completes with zero candidates, a synthetic srflx candidate using webrtc_public_ip is injected into both the onicecandidate event and localDescription.sdp.

Why it is needed

WebRTC is the primary technique for bypassing proxy IP masking. Even with an HTTP proxy, a STUN request can return the real machine IP. CreepJS, BrowserScan, and others specifically probe RTCPeerConnection and compare the result against the HTTP-observed IP. The four-layer approach handles all detection paths: the ICE event, the local SDP string, and the absence of any candidates at all.

22

Battery API Spoofing

What it does

Two modes controlled by battery_mode: "absent"navigator.getBattery() rejects with NotSupportedError, mimicking Firefox or a hardened profile that has disabled the API. "stable" — returns charging = true, level = 1.0, chargingTime = 0, dischargingTime = Infinity, matching a laptop connected to AC power.

Why it is needed

A headless server has no battery. getBattery() returning null or an object with unusual values (level=1, dischargingTime=Infinity but charging=false) is a well-known headless tell. "stable" mode produces the values that BrowserLeaks and similar services classify as benign.

23

Speech Synthesis Voice List Spoofing

What it does

When the FingerprintConfig contains a parsed voices_parsed list (derived from voices_json), the platform's TTS voice list is replaced entirely with synthetic SpeechSynthesisVoice objects constructed from the configured data. A fallback path in getVoices() handles the case where the TTS backend never delivers a voice list at all (common on headless Linux).

Why it is needed

speechSynthesis.getVoices() returns a platform-specific list. On Linux the list contains eSpeak entries; on Windows it contains Microsoft voices; on macOS it contains Apple voices. Returning the wrong platform's voice list immediately contradicts the spoofed operating system.

24

Media Devices Stub Injection

What it does

After enumerateDevices() resolves, if the resulting list is empty and a FingerprintConfig is active, three stub MediaDeviceInfo entries are injected: one audioinput, one videoinput, one audiooutput, all with empty deviceId, label, and groupId (matching real Chrome before a getUserMedia grant).

Why it is needed

A headless Chromium instance on a server has no audio or video hardware and returns an empty device list. Real Chrome always returns at least one stub device per category even before permissions are granted. An empty list is a well-known headless signal.

25

navigator.connection.downlinkMax Fix

What it does

Returns Infinity for downlinkMax whenever a FingerprintConfig is active, regardless of the actual network state. Runtime feature flags NetInfoDownlinkMax, ContentIndex, and ContactsManager are also enabled in SetFingerprintConfig().

Why it is needed

CreepJS reports noDownlinkMax as a headless signal when downlinkMax is 0 or undefined. Real Chrome on WiFi/Ethernet returns Infinity.

26

chrome.app and chrome.runtime Object Stubs

What it does

Adds JavaScript that installs fully-shaped chrome.app and chrome.runtime objects (with all enum properties and method stubs) into the page context if they are not already present. These stubs are idempotent — real extension objects are preserved if already installed.

Why it is needed

Headless Chrome and some automation wrappers expose an incomplete chrome global. Detectors like BotD, FpScanner, and headless-cat-and-mouse probe the shape of chrome.app and chrome.runtime and flag absent or incomplete objects. The stubs match exactly the shape of those objects in a normal desktop Chrome session.

27

isTrusted Spoofing for CDP-Dispatched Events

What it does

When humanize_cdp_events is set in the FingerprintConfig, events dispatched from page JavaScript (including scripts injected via Runtime.evaluate) are marked isTrusted = true instead of the W3C-mandated false.

Why it is needed

reCAPTCHA v3, Cloudflare Turnstile, and similar challenge systems inspect event.isTrusted on click, keydown, and input events. CDP-automated interactions (which include Playwright's click simulation) always produce isTrusted = false. This flag allows humanization scripts to produce events that appear natively generated.

28

Storage Quota Spoofing

What it does

When storage_quota_mb is set, StorageManager.estimate() returns the configured value as the quota. If the real used storage exceeds the spoofed quota, usage is clamped to the quota value to prevent usage > quota (which is itself a detection signal).

Why it is needed

FingerprintJS and CreepJS read navigator.storage.estimate(). In incognito mode or on a freshly created profile, Chrome returns a small quota (~120 MB). On a normal profile it returns a quota proportional to disk space (often several GB). A mismatch with the spoofed profile type is a detection signal.

29

Permissions API Normalization

What it does

After the permissions service resolves a query, if the result is "denied" for notifications, clipboard-read, or clipboard-write, the status is remapped to "prompt" (ASK) before being returned to JavaScript.

Why it is needed

The most widely used "headless detection in 7 lines" probe queries navigator.permissions.query({name:'notifications'}) and checks for "denied". A fresh, non-headless Chrome profile returns "default" (which maps to "prompt" in PermissionStatus). Headless / automation profiles are frequently in a pre-denied state that matches the server's no-notification environment.

30

navigator.plugins / PDF Viewer Restoration

What it does

Forces the canonical 5-entry hardcoded PDF viewer plugin list (introduced by the browser in 2021) to be seeded into DOMPluginArray when expose_default_plugins is set (default true), bypassing the IsPdfViewerAvailable() check that can return false in headless renderers.

Why it is needed

navigator.plugins.length === 0 is a strong headless signal. Real Chrome always exposes the PDF viewer plugin entries even with PDF disabled. Forcing them present closes this detection vector.

31

Font Enumeration Allowlist (queryLocalFonts)

What it does

When allowed_fonts is populated, window.queryLocalFonts() resolves immediately with synthetic FontMetadata entries for exactly those families, skipping the browser-process permission prompt entirely. If the caller passes a postscriptNames filter, only the intersection is returned.

Why it is needed

The Local Font Access API exposes the full list of installed fonts, which is highly machine-specific. Returning a controlled list makes the API consistent with the spoofed operating system without triggering a permission prompt that would alert the user.

32

CSS Font Detection via measureText Aliasing (Windows Only)

What it does

When a CSS font lookup requests a family in the allowed_fonts allowlist but the family is not physically installed on the host machine, the lookup is aliased to a deterministically-chosen fallback from a pool of fonts that ship with Windows 10/11. The alias is computed via FNV-1a hash on the family name, so each allowed family maps to a unique pool member and produces a distinct measureText width.

Why it is needed

CSS-based font detection (used by CreepJS and FingerprintJS) renders a test string in each candidate font and compares the measureText width against a generic fallback. If the widths match, the font is absent. Aliasing to installed fonts makes every allowlisted family appear present with a unique, stable width without requiring those fonts to be physically installed on the dev/server machine.

33

Proxy Timing Concealment (--liinked-proxy-timing)

What it does

When --liinked-proxy-timing is passed on the command line: (1) The Proxy-Connection: keep-alive HTTP/1.1 header is suppressed and replaced with a standard Connection: keep-alive, removing the HTTP proxy tell from the request. (2) PerformanceResourceTiming connect and SSL timing fields are collapsed: the SSL window is zeroed and connectEnd is set equal to connectStart, making proxied requests appear to have the same timing profile as direct connections or socket-reused loads.

Why it is needed

Akamai Bot Manager and other CDN-level bot detection systems examine request headers and PerformanceResourceTiming data. The presence of Proxy-Connection or an anomalous gap between connectStart and connectEnd (caused by the CONNECT tunnel handshake) can identify proxied traffic even when the proxy is otherwise transparent.

34

HTTP/2 Fingerprint Audit Marker

What it does

A no-op comment block is placed at the top of SendInitialData(), the single function responsible for Chrome's HTTP/2 connection preface, initial SETTINGS frame, and initial WINDOW_UPDATE frame.

Why it is needed

TLS/H2 fingerprinting services (e.g. tls.peet.ws, Akamai) record the exact SETTINGS keys, their values, their order, and the WINDOW_UPDATE delta as a browser identity signal. Any accidental change during a Chromium uprev that perturbs these values would break the H2 signature. The marker ensures a reviewer notices this function when merging future upstream changes.

35

IP / Locale Consistency Diagnostic

What it does

At browser startup, after profiles are loaded, a background check fetches the public IP geolocation and compares the reported country/timezone against the active profile's configured timezone and language settings. A WARNING is logged if there is a mismatch.

Why it is needed

If the proxy's exit IP resolves to Germany but the profile claims timezone America/New_York and language en-US, every geolocation-aware fingerprinting service will flag the inconsistency. This diagnostic surfaces misconfigurations early during development and QA without requiring a full bot-detection run.

36

FingerprintConfig IPC Plumbing

What it does

Wires the entire config distribution pipeline: RenderProcessHostImpl::Init() calls SendFingerprintConfig() which loads the config, caches it, pushes it to the browser-process global, serializes it into a content::mojom::FingerprintConfig Mojo struct, and sends it to the renderer. RenderThreadImpl::SetFingerprintConfig() receives it, copies all fields into a blink::WebFingerprintConfig, calls blink::WebSetFingerprintConfig(), and also applies the timezone override and enables the required runtime feature flags.

Why it is needed

This is the connective tissue that makes every other modification work. Without it the per-profile data in the browser process would never reach the renderer-side spoofing hooks.

Private developer preview

Building Chromium automation infrastructure?

Request access and share your profile, proxy, session, and fingerprinting requirements.

Request beta access