browser-commander 0.11.0

Universal browser automation library that supports multiple browser engines with a unified API
Documentation
[
  {
    "id": "automation-controlled-is-launch-only",
    "surface": "navigator.webdriver",
    "severity": "high",
    "evidence": "measured",
    "detail": "navigator.webdriver is the AutomationControlled Blink runtime feature. Chrome enables it for --enable-automation, --headless, --remote-debugging-pipe and an ephemeral --remote-debugging-port=0, and the only way to turn it back off is the launch switch --disable-blink-features=AutomationControlled. Emulation.setAutomationOverride({enabled:false}) is accepted over CDP but leaves navigator.webdriver true, in the same document and after navigation, so a browser somebody else already launched cannot be corrected from the protocol side.",
    "workaround": "Launch the browser through this library, which passes the switch. When attaching to a browser somebody else started, apply the profile with the webdriver patch option, which installs a JavaScript getter instead: that is weaker, because it does not reach workers or a renderer that starts before the patch is installed.",
    "reference": "content/child/runtime_features.cc; analysis-artifacts/remote-debugging-isolation.json"
  },
  {
    "id": "no-cdp-device-memory-override",
    "surface": "navigator.deviceMemory",
    "severity": "medium",
    "evidence": "measured",
    "detail": "The Emulation domain has no deviceMemory command, so the value can only be patched in JavaScript. Workers and any code that reads the value before the init script runs still see the real amount of memory, rounded to the nearest power of two as the specification requires.",
    "workaround": "Choose a profile whose deviceMemory matches the host where possible.",
    "reference": "analysis-artifacts/cdp-override-coverage.json"
  },
  {
    "id": "no-cdp-vendor-or-dnt-override",
    "surface": "navigator.vendor, navigator.doNotTrack",
    "severity": "low",
    "evidence": "measured",
    "detail": "Neither has an Emulation command; both are JavaScript patches only. navigator.vendor is \"Google Inc.\" on every Chromium build, so it rarely needs changing, but a profile that claims a non-Chromium browser has to patch it and the patch does not reach workers.",
    "reference": "analysis-artifacts/cdp-override-coverage.json"
  },
  {
    "id": "screen-depth-and-avail-not-emulated",
    "surface": "screen.colorDepth, screen.pixelDepth, screen.availWidth, screen.availHeight",
    "severity": "low",
    "evidence": "measured",
    "detail": "Emulation.setDeviceMetricsOverride controls screen.width and screen.height only. It sets availWidth and availHeight equal to them, so a profile that models an operating system taskbar has to patch those in JavaScript, and the colour depths are not emulated at all.",
    "reference": "analysis-artifacts/cdp-override-coverage.json"
  },
  {
    "id": "webgl-strings-only",
    "surface": "WebGL renderer strings and driver limits",
    "severity": "high",
    "evidence": "documented",
    "detail": "The unmasked vendor and renderer strings can be replaced in JavaScript, but the numeric driver limits next to them -- MAX_TEXTURE_SIZE, ALIASED_LINE_WIDTH_RANGE, the bit depths, the supported extension list -- come from the real GPU stack. Claiming an Apple GPU while reporting Mesa's limits is more identifying than not claiming anything.",
    "workaround": "Either leave the WebGL strings alone or run on hardware that matches the profile you are claiming."
  },
  {
    "id": "canvas-audio-font-follow-the-host",
    "surface": "canvas and audio digests, font metrics",
    "severity": "high",
    "evidence": "documented",
    "detail": "These are produced by the host GPU, audio stack and installed fonts. Browser Commander does not perturb them, because the usual countermeasure -- adding per-session noise -- is itself detectable: a real browser returns the same digest twice in a row, and a noised one does not.",
    "workaround": "Match the host environment to the profile, for example by running in a container image with the font set you intend to claim."
  },
  {
    "id": "grease-brand-not-reproduced",
    "surface": "navigator.userAgentData.brands ordering",
    "severity": "low",
    "evidence": "documented",
    "detail": "Chrome generates the GREASE entry in Sec-CH-UA from a per-version permutation of separators and ordering. Derived profiles use the common \"Not=A?Brand\";v=\"24\" form in a fixed position, which will not match every Chrome build exactly.",
    "workaround": "Pass an explicit userAgentData.brands copied from the browser build you are modelling.",
    "reference": "https://wicg.github.io/ua-client-hints/"
  },
  {
    "id": "touch-emulation-changes-pointer-media",
    "surface": "(pointer) and (hover) media queries",
    "severity": "medium",
    "evidence": "measured",
    "detail": "Setting maxTouchPoints above zero goes through Emulation.setTouchEmulationEnabled, which also makes the primary pointer coarse and removes hover. That is correct for a phone and wrong for a desktop that happens to have a touchscreen.",
    "workaround": "Leave maxTouchPoints unset on desktop profiles unless the pointer change is what you want.",
    "reference": "analysis-artifacts/cdp-override-coverage.json"
  },
  {
    "id": "headless-is-distinguishable",
    "surface": "user agent, screen size, hover and pointer media queries, WebGL renderer",
    "severity": "high",
    "evidence": "measured",
    "detail": "A real headless Chrome differs from a real headful Chrome in thirteen probe fields with no automation involved at all: the user agent and appVersion contain \"HeadlessChrome\" -- in the document, in an iframe and in a worker -- the screen is 800x600 and availWidth and availHeight follow it, the four hover and pointer media queries report no hover and no fine pointer, and WebGL falls back to SwiftShader. A headless browser is therefore compared against a headless reference, never against a headful one.",
    "workaround": "Run headful, under a virtual display such as Xvfb if needed.",
    "reference": "analysis-artifacts/parity-headless.json"
  },
  {
    "id": "network-layer-not-covered",
    "surface": "TLS and HTTP/2 fingerprints (JA3, JA4, Akamai h2 fingerprint)",
    "severity": "high",
    "evidence": "documented",
    "detail": "The TLS ClientHello and the HTTP/2 SETTINGS frame identify the network stack, not the JavaScript environment, and CDP exposes no way to change either. They are identical between a real Chrome and an automated Chrome of the same build, so they are not an automation signal -- but they do pin the browser to a real Chrome version, and a profile that claims a different browser or version will not match at that layer.",
    "workaround": "Keep the claimed browser and version close to the browser actually running."
  },
  {
    "id": "init-script-does-not-reach-workers",
    "surface": "Values patched in JavaScript, as seen from a Worker",
    "severity": "medium",
    "evidence": "measured",
    "detail": "A worker has its own Navigator, and overrides set on the page session only partly reach it. Measured in a dedicated worker: userAgent, timezone and locale follow the profile, but platform, language, languages, hardwareConcurrency and deviceMemory all report the real host values, and no JavaScript patch is present because Page.addScriptToEvaluateOnNewDocument runs in documents only. Full worker parity is reachable over raw CDP -- replaying the Emulation commands on the worker session fixes languages and hardwareConcurrency, and evaluating the init script during the waitForDebuggerOnStart pause fixes the rest -- but that needs a sessionId, which neither engine accepts: Playwright newCDPSession takes only Page or Frame, and Puppeteer worker sessions are internal to its TargetManager.",
    "workaround": "Avoid workloads that read the fingerprint from a worker, or drive Chrome over a raw CDP connection and replay the commands the CDP override builder returns on each attached worker session.",
    "reference": "analysis-artifacts/worker-visibility.json"
  }
]