<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>liarjs.dev · field notes</title><description>Deep-dives on browser fingerprinting and anti-detect technology: canvas, WebGL/WebGPU, fonts, audio, TLS/JA3, cross-consistency and how modern bot detection really works.</description><link>https://liarjs.dev/</link><language>en-us</language><atom:link href="https://liarjs.dev/rss.xml" rel="self" type="application/rss+xml"/><item><title>The anti-detect browser Royal Rumble: what independent testing says vs the marketing</title><link>https://liarjs.dev/blog/anti-detect-royal-rumble/</link><guid isPermaLink="true">https://liarjs.dev/blog/anti-detect-royal-rumble/</guid><description>Every vendor publishes a table where it wins. The one independent, controlled comparison inverts the marketing order, and the open-source tools tell a different story again.</description><pubDate>Sat, 31 Oct 2026 00:00:00 GMT</pubDate><category>Benchmarks</category><category>benchmark</category><category>gologin</category><category>kameleo</category><category>octo</category><category>nodriver</category><category>patchright</category></item><item><title>Emoji are an operating system: rasterisation, ZWJ sequences and the font you cannot ship</title><link>https://liarjs.dev/blog/emoji-rasterisation/</link><guid isPermaLink="true">https://liarjs.dev/blog/emoji-rasterisation/</guid><description>Apple Color Emoji, Segoe UI Emoji and Noto Color Emoji are three different fonts with three different pictures for the same codepoints. None of them is redistributable except the last, and canvas reads all of them.</description><pubDate>Wed, 28 Oct 2026 00:00:00 GMT</pubDate><category>Fonts</category><category>emoji</category><category>canvas</category><category>zwj</category><category>os-detection</category></item><item><title>Detection strength is the vendor stack: Kasada, Akamai, DataDome and the ladder</title><link>https://liarjs.dev/blog/anti-bot-vendor-stack/</link><guid isPermaLink="true">https://liarjs.dev/blog/anti-bot-vendor-stack/</guid><description>A site isn&apos;t hard because of the site. It&apos;s hard because of the anti-bot vendor behind it. Here&apos;s the difficulty ladder and the signature artifacts of each stack.</description><pubDate>Sat, 24 Oct 2026 00:00:00 GMT</pubDate><category>Bot detection</category><category>kasada</category><category>akamai</category><category>datadome</category><category>cloudflare</category><category>perimeterx</category><category>imperva</category></item><item><title>getLayoutMap(): the physical keyboard as an OS and region signal</title><link>https://liarjs.dev/blog/keyboard-layout-map/</link><guid isPermaLink="true">https://liarjs.dev/blog/keyboard-layout-map/</guid><description>One promise, no permission prompt, and out comes a map of every key on the keyboard to the character it produces. It names your layout, and one entry names your operating system outright.</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><category>Locale</category><category>keyboard</category><category>layout</category><category>region</category><category>chromium-only</category></item><item><title>WebRTC IP leaks: ICE candidates, mDNS and the public-IP mismatch</title><link>https://liarjs.dev/blog/webrtc-ip-leaks/</link><guid isPermaLink="true">https://liarjs.dev/blog/webrtc-ip-leaks/</guid><description>WebRTC can hand a site your real IP from behind the proxy through ICE candidates. Blocking it is not the fix. The fix is making the exposed IP agree with your connection.</description><pubDate>Sat, 17 Oct 2026 00:00:00 GMT</pubDate><category>WebRTC</category><category>ice</category><category>mdns</category><category>ip-leak</category><category>proxy</category></item><item><title>Beyond width: the other eleven numbers TextMetrics gives you</title><link>https://liarjs.dev/blog/textmetrics-beyond-width/</link><guid isPermaLink="true">https://liarjs.dev/blog/textmetrics-beyond-width/</guid><description>Metric-compatible substitutes match advance widths by design. They do not match the font bounding box, the ink extents or the baselines, and measureText returns all of those from the same call.</description><pubDate>Wed, 14 Oct 2026 00:00:00 GMT</pubDate><category>Fonts</category><category>textmetrics</category><category>measuretext</category><category>metric-compatible</category><category>baselines</category></item><item><title>The layer JavaScript never sees: TLS, JA3/JA4 and HTTP/2 fingerprints</title><link>https://liarjs.dev/blog/tls-ja3-ja4-network-layer/</link><guid isPermaLink="true">https://liarjs.dev/blog/tls-ja3-ja4-network-layer/</guid><description>Before a line of your JS runs, the TLS handshake and HTTP/2 frames have already fingerprinted the client. It&apos;s the wall a JS-layer spoof can&apos;t touch and the place where a real Chrome build pays off.</description><pubDate>Sat, 10 Oct 2026 00:00:00 GMT</pubDate><category>Network layer</category><category>tls</category><category>ja3</category><category>ja4</category><category>http2</category><category>ja4h</category><category>network</category></item><item><title>Three majors behind: version lag is a fingerprint of its own</title><link>https://liarjs.dev/blog/version-lag-fingerprint/</link><guid isPermaLink="true">https://liarjs.dev/blog/version-lag-fingerprint/</guid><description>Chrome ships a major every four weeks and updates itself without asking. A browser several versions back is not a user who forgot, it is a pinned build, and that narrows the anonymity set before anything else is read.</description><pubDate>Wed, 07 Oct 2026 00:00:00 GMT</pubDate><category>Engineering</category><category>version</category><category>user-agent</category><category>client-hints</category><category>anonymity-set</category><category>release-cadence</category></item><item><title>DOMRect and subpixel geometry: the fingerprint DataDome loves and the dpr trap that breaks spoofers</title><link>https://liarjs.dev/blog/domrect-subpixel/</link><guid isPermaLink="true">https://liarjs.dev/blog/domrect-subpixel/</guid><description>getClientRects returns floating-point layout geometry that varies by device and CreepJS hides a hardcoded hash that only fires when devicePixelRatio equals 1.</description><pubDate>Sat, 03 Oct 2026 00:00:00 GMT</pubDate><category>Geometry</category><category>domrect</category><category>getclientrects</category><category>subpixel</category><category>datadome</category><category>devicepixelratio</category></item><item><title>Which signal actually blocked you: running a controlled bisect</title><link>https://liarjs.dev/blog/bisect-the-block/</link><guid isPermaLink="true">https://liarjs.dev/blog/bisect-the-block/</guid><description>A rejection tells you nothing about which of forty signals caused it. One variable at a time, a control arm on both ends, and the discipline to accept that four fixes in a row changed nothing.</description><pubDate>Wed, 30 Sep 2026 00:00:00 GMT</pubDate><category>Method</category><category>methodology</category><category>bisect</category><category>debugging</category><category>detection</category><category>experiment-design</category></item><item><title>Workers and OffscreenCanvas: fingerprinting in the thread you forgot to patch</title><link>https://liarjs.dev/blog/workers-offscreencanvas/</link><guid isPermaLink="true">https://liarjs.dev/blog/workers-offscreencanvas/</guid><description>Detectors re-read identity inside a Web Worker and re-run canvas in OffscreenCanvas, a favorite way to catch spoofs that only hooked the main thread.</description><pubDate>Sat, 26 Sep 2026 00:00:00 GMT</pubDate><category>Workers</category><category>offscreencanvas</category><category>workernavigator</category><category>consistency</category></item><item><title>The plugin face Android never had: five entries that kill a mobile persona</title><link>https://liarjs.dev/blog/android-plugin-face/</link><guid isPermaLink="true">https://liarjs.dev/blog/android-plugin-face/</guid><description>Chrome on Android is built without the PDF viewer, so navigator.plugins is empty, mimeTypes is empty and pdfViewerEnabled is false. A desktop build wearing a phone persona reports 5, 2 and true.</description><pubDate>Wed, 23 Sep 2026 00:00:00 GMT</pubDate><category>Navigator</category><category>plugins</category><category>pdf</category><category>android</category><category>mobile</category><category>build-flags</category></item><item><title>The plugin face: modern Chrome&apos;s five PDF entries and the version it betrays</title><link>https://liarjs.dev/blog/plugin-face-version-consistency/</link><guid isPermaLink="true">https://liarjs.dev/blog/plugin-face-version-consistency/</guid><description>navigator.plugins looks vestigial, but its exact shape is pinned to the Chrome version, and a stale plugin face is a clean contradiction detectors love.</description><pubDate>Sat, 19 Sep 2026 00:00:00 GMT</pubDate><category>Navigator</category><category>plugins</category><category>mimetypes</category><category>version-consistency</category><category>uach</category></item><item><title>Static says phone, events say mouse: the device-class contradiction</title><link>https://liarjs.dev/blog/touch-that-never-touches/</link><guid isPermaLink="true">https://liarjs.dev/blog/touch-that-never-touches/</guid><description>A mobile persona sets maxTouchPoints and pointer:coarse and passes every static check. Then the page listens, and every event that arrives is a mouse event from a machine with a mouse.</description><pubDate>Wed, 16 Sep 2026 00:00:00 GMT</pubDate><category>Behavior</category><category>touch</category><category>pointer-events</category><category>mobile</category><category>coalesced-events</category><category>istrusted</category></item><item><title>[native code]: why JS-override stealth loses to engine-level spoofing</title><link>https://liarjs.dev/blog/native-function-integrity/</link><guid isPermaLink="true">https://liarjs.dev/blog/native-function-integrity/</guid><description>Every getter you redefine in JavaScript leaves a residue a lie-detector can read. The split between JS-patched and engine-level browsers shows up in one toString() call.</description><pubDate>Sat, 12 Sep 2026 00:00:00 GMT</pubDate><category>Architecture</category><category>stealth</category><category>toString</category><category>proxy</category><category>engine-level</category><category>detection</category></item><item><title>Mobile texture formats on a desktop GPU: the capability table under the string</title><link>https://liarjs.dev/blog/gpu-capability-tables/</link><guid isPermaLink="true">https://liarjs.dev/blog/gpu-capability-tables/</guid><description>Rewriting UNMASKED_RENDERER takes one line. The extension list, the WebGPU feature set and the shader precision table underneath it are produced by the driver, and they name the chip that is really there.</description><pubDate>Wed, 09 Sep 2026 00:00:00 GMT</pubDate><category>WebGL</category><category>webgpu</category><category>extensions</category><category>angle</category><category>form-factor</category></item><item><title>Timezone vs IP: the cross-layer check that catches proxies</title><link>https://liarjs.dev/blog/timezone-vs-ip/</link><guid isPermaLink="true">https://liarjs.dev/blog/timezone-vs-ip/</guid><description>A US exit IP paired with an Asia/Singapore browser clock is one of the cheapest, most reliable bot tells there is, and two well-funded anti-detect browsers shipped exactly that.</description><pubDate>Sat, 05 Sep 2026 00:00:00 GMT</pubDate><category>Geolocation</category><category>timezone</category><category>geoip</category><category>intl</category><category>proxy</category><category>consistency</category></item><item><title>A window cannot be bigger than its screen: the geometry invariants</title><link>https://liarjs.dev/blog/window-screen-geometry/</link><guid isPermaLink="true">https://liarjs.dev/blog/window-screen-geometry/</guid><description>Screen spoofing usually rewrites screen.width and stops there. The window, its position, the menu bar inset and the multi-monitor flag all keep describing the real display, and they have to agree with each other.</description><pubDate>Wed, 02 Sep 2026 00:00:00 GMT</pubDate><category>Screen</category><category>viewport</category><category>geometry</category><category>isextended</category><category>window-management</category></item><item><title>Audio fingerprinting: the oscillator, the compressor and why silence must stay silent</title><link>https://liarjs.dev/blog/audio-fingerprinting-silence/</link><guid isPermaLink="true">https://liarjs.dev/blog/audio-fingerprinting-silence/</guid><description>The Web Audio fingerprint is stable to many decimal places and the fastest way to fail it is to add noise where a real browser outputs pure zeros.</description><pubDate>Sat, 29 Aug 2026 00:00:00 GMT</pubDate><category>Audio</category><category>webaudio</category><category>offlineaudiocontext</category><category>compressor</category><category>noise</category></item><item><title>The interface census: an Android persona is a different build, not a different User-Agent</title><link>https://liarjs.dev/blog/api-surface-census/</link><guid isPermaLink="true">https://liarjs.dev/blog/api-surface-census/</guid><description>Chromium decides which Web APIs exist per platform, at build and configuration time. A desktop binary running a phone persona still answers typeof navigator.hid with &apos;object&apos;, and no identity layer moves that answer.</description><pubDate>Wed, 26 Aug 2026 00:00:00 GMT</pubDate><category>Mobile</category><category>api-surface</category><category>android</category><category>persona</category><category>form-factor</category><category>runtime-features</category></item><item><title>Named but not measurable: the font check every cross-OS persona fails</title><link>https://liarjs.dev/blog/font-metrics-collapse/</link><guid isPermaLink="true">https://liarjs.dev/blog/font-metrics-collapse/</guid><description>An allow-list can put Segoe UI in the enumerable font set on a Mac. It cannot give it metrics. Every family the host has no file for measures the same width, and that one number is enough to call the persona a lie.</description><pubDate>Wed, 19 Aug 2026 00:00:00 GMT</pubDate><category>Fonts</category><category>measuretext</category><category>cross-os</category><category>persona</category><category>host-leaks</category></item><item><title>WebGPU, the new GPU oracle: adapter.info and the cross-interface triad</title><link>https://liarjs.dev/blog/webgpu-gpu-triad/</link><guid isPermaLink="true">https://liarjs.dev/blog/webgpu-gpu-triad/</guid><description>WebGPU exposes a second, independent view of your GPU. If it disagrees with WebGL and the UA platform, the spoof falls apart. Most anti-detect browsers shipped that mismatch for years.</description><pubDate>Wed, 19 Aug 2026 00:00:00 GMT</pubDate><category>WebGPU</category><category>gpu</category><category>adapter-info</category><category>consistency</category><category>cross-interface</category></item><item><title>WebGL fingerprinting: it&apos;s the whole parameter set, not just the renderer string</title><link>https://liarjs.dev/blog/webgl-whole-parameter-set/</link><guid isPermaLink="true">https://liarjs.dev/blog/webgl-whole-parameter-set/</guid><description>Spoofing UNMASKED_RENDERER is the easy 5%. Detectors read dozens of getParameter values, shader precision and the actual rendered pixels and &apos;self-consistent&apos; is not the same as &apos;believable&apos;.</description><pubDate>Wed, 12 Aug 2026 00:00:00 GMT</pubDate><category>WebGL</category><category>gpu</category><category>getparameter</category><category>readpixels</category><category>self-consistency</category></item><item><title>Automation tells are orthogonal: CDP, Runtime.enable and the isolated world</title><link>https://liarjs.dev/blog/automation-cdp-tells/</link><guid isPermaLink="true">https://liarjs.dev/blog/automation-cdp-tells/</guid><description>A flawless fingerprint won&apos;t hide the driver. navigator.webdriver, CDP execution-context leaks and headless tokens are a separate axis every serious detector checks.</description><pubDate>Fri, 07 Aug 2026 00:00:00 GMT</pubDate><category>Automation</category><category>cdp</category><category>webdriver</category><category>playwright</category><category>puppeteer</category><category>rebrowser</category><category>headless</category></item><item><title>Font fingerprinting without a font API: the offsetWidth measurement method</title><link>https://liarjs.dev/blog/font-fingerprinting-offsetwidth/</link><guid isPermaLink="true">https://liarjs.dev/blog/font-fingerprinting-offsetwidth/</guid><description>The dominant font-enumeration technique never calls a font API at all. It measures span widths. That is why patching document.fonts alone gets you caught, and why CJK fonts leak your real OS.</description><pubDate>Wed, 05 Aug 2026 00:00:00 GMT</pubDate><category>Fonts</category><category>offsetwidth</category><category>measuretext</category><category>cjk</category><category>enumeration</category></item><item><title>Inside the CreepJS lie detector: four checks deterministic noise can&apos;t beat</title><link>https://liarjs.dev/blog/creepjs-lie-detector/</link><guid isPermaLink="true">https://liarjs.dev/blog/creepjs-lie-detector/</guid><description>CreepJS doesn&apos;t only collect signals. It proves them self-consistent. Here are the four invariant classes that catch spoofers even when their noise is perfectly stable.</description><pubDate>Wed, 29 Jul 2026 00:00:00 GMT</pubDate><category>Lie detection</category><category>creepjs</category><category>lies</category><category>invariants</category><category>canvas</category><category>audio</category><category>domrect</category></item><item><title>Canvas fingerprinting and the deterministic-noise rule</title><link>https://liarjs.dev/blog/canvas-deterministic-noise/</link><guid isPermaLink="true">https://liarjs.dev/blog/canvas-deterministic-noise/</guid><description>Anti-detect browsers get caught on canvas because their noise changes between two identical reads. The size of the noise is almost beside the point. Determinism is the whole game.</description><pubDate>Wed, 22 Jul 2026 00:00:00 GMT</pubDate><category>Canvas</category><category>noise</category><category>creepjs</category><category>determinism</category></item><item><title>Two layers, one browser: why consistency beats any single fingerprint</title><link>https://liarjs.dev/blog/two-layers-cross-consistency/</link><guid isPermaLink="true">https://liarjs.dev/blog/two-layers-cross-consistency/</guid><description>A perfect JavaScript fingerprint still fails if it disagrees with the network layer. Modern detection is a cross-layer consistency problem, not a collection problem.</description><pubDate>Wed, 15 Jul 2026 00:00:00 GMT</pubDate><category>Foundations</category><category>consistency</category><category>cross-layer</category><category>detection</category><category>tls</category></item></channel></rss>