# Stealth Patches `retoor ` This document enumerates every modification stealthii applies to the underlying Chromium/Playwright stack and the rationale for each. It is maintained as living documentation: any change to `stealth_js.py` or the launch arguments in `client.py` must be reflected here in the same commit. ## Launch-level patches Applied in `Stealthii._ensure_browser_locked()` at browser launch. | Flag | Effect | |---|---| | `--disable-blink-features=AutomationControlled` | Removes the `navigator.webdriver` flag and associated automation-only code paths at the engine level, before any JS patch runs. | | `ignore_default_args=['--enable-automation']` | Playwright passes `--enable-automation` to Chromium by default, which enables the automation infobar and related detectable behaviour. Suppressed. | | `--no-sandbox` | Required to run headless Chromium as a non-root, unprivileged process in this deployment environment. Not a stealth measure. | | `--disable-dev-shm-usage` | Avoids `/dev/shm` exhaustion under containerized/limited-memory environments. Not a stealth measure. | | `--disk-cache-size=` / `--media-cache-size=` | Bounds Chromium's own on-disk HTTP/media cache, configurable via `disk_cache_bytes` (default near-zero). Not a stealth measure — an operational safeguard against unbounded disk growth in a long-lived process. | ## JS-level patches Applied via `context.add_init_script(STEALTH_INIT_JS)` on every context before any page script runs, so the patched state is present from the very first document, including cross-origin iframes. ### `navigator.webdriver` Redefined to return `undefined`. This flag is `true` on any CDP-automated Chromium and is the single most widely checked automation signal. ### `window.chrome` A stock headless Chromium launched via Playwright does not expose `window.chrome` at all, unlike a real Chrome install. When absent, a `window.chrome` object is constructed with `runtime`, `loadTimes`, `csi`, and `app` members shaped like the real object's public surface. ### `navigator.permissions.query` A stock headless Chromium reports `denied` for a `notifications` permission query while `Notification.permission` itself still reports `default` — a mismatch a real browser never produces. The query is patched to always return the same value as `Notification.permission` for that one permission name; all other permission names are passed through to the original implementation unmodified. ### `navigator.plugins` / `navigator.mimeTypes` A stock headless Chromium reports an empty plugin list and a `navigator.plugins` value that is a plain array rather than a `PluginArray`. Five entries matching Chrome's real built-in PDF-plugin set (`PDF Viewer`, `Chrome PDF Viewer`, `Chromium PDF Viewer`, `Microsoft Edge PDF Viewer`, `WebKit built-in PDF`) are constructed, each individually reassigned the real `Plugin.prototype`, and the containing array reassigned `PluginArray.prototype` — including `Symbol.toStringTag`, so both a property count check and a `Object.prototype.toString.call(...)` type check pass. `navigator.mimeTypes` is patched the same way for the corresponding `application/pdf` MIME type. ### `navigator.deviceMemory`, `navigator.hardwareConcurrency` Set to `8` each. Present mainly for consistency with the spoofed WebGL GPU (below) and a realistic desktop profile; a stock headless Chromium already reports plausible values here in most cases. ### `WebGLRenderingContext.prototype.getParameter` / `WebGL2RenderingContext.prototype.getParameter` A stock headless Chromium renders WebGL through SwiftShader (a software rasterizer), which reports `UNMASKED_VENDOR_WEBGL` (constant `37445`) as `Google Inc. (Google)` and `UNMASKED_RENDERER_WEBGL` (constant `37446`) containing the literal string `SwiftShader` — an unambiguous headless signal checked by essentially every fingerprinting script. Both constants are intercepted and rewritten to report a real discrete GPU (`Google Inc. (NVIDIA)` / an ANGLE-wrapped `NVIDIA GeForce RTX 3060` string); every other parameter is passed through unmodified. ### `HTMLIFrameElement.prototype.contentWindow` A known secondary check reads `window.chrome` from inside a freshly created, same-origin iframe rather than the top-level document — since each document gets its own JS globals, an iframe's `window.chrome` can be absent even when the top-level one was patched. The `contentWindow` getter is wrapped so that, whenever the returned window is missing `chrome`, it is assigned from the outer patched value before being returned. ### `Function.prototype.toString` spoofing on every patch above Every function or getter installed by the patches above is wrapped in a `Proxy` whose own `toString` is overridden to return the exact string a real native function produces (`function name() { [native code] }`). Without this, a page can distinguish a patched native method from a real one by calling `.toString()` on it and observing actual JavaScript source instead of the native-code marker — a check used by several stealth- detection scripts specifically to unmask other stealth patches. ## Verification Patches are verified against [bot.sannysoft.com](https://bot.sannysoft.com)'s full detection table (`tests/test_stealth_detection.py::test_sannysoft_detection_suite_is_clean`), run on every change. As of the current `STEALTH_INIT_JS`, every row on that page passes, including `WebDriver (New)`, `Chrome (New)`, `Plugins is of type PluginArray`, `WebGL Vendor`/`WebGL Renderer`, and the `HEADCHR_*`/`PHANTOM_*`/`SELENIUM_DRIVER` probe blocks. The `get`/`post`/`put`/`patch`/`delete`/`head`/`download` fast path (no tab, `context.request`) is separately verified (`test_fast_path_uses_real_chromium_fingerprint`) to carry a JA3/JA4 hash distinct from a plain Python `aiohttp` client, confirming it uses the browser's own TLS stack rather than Python's. ## Known limitations - The `context.request` fast path negotiates HTTP/1.1, not HTTP/2 — a real Chrome page load always uses HTTP/2 against an HTTP/2-capable server. A system that cross-checks the TLS fingerprint against the negotiated protocol can detect this specific inconsistency. `render()` (full page navigation) does not have this limitation. - None of the above defeats IP-reputation-based rate limiting, nor the behavioural layer of systems such as DataDome or a Cloudflare managed challenge. Those require a clean source IP and, for the hardest targets, human-like interaction; stealthii's patches remove fingerprint-level tells, not behavioural ones. - Verified against Chromium's stock headless build as shipped with Playwright at the time of writing. A Chromium upgrade can change default values the patches assume (plugin list shape, WebGL constants, permission defaults); re-run the sannysoft regression test after any Playwright/Chromium version bump.