DEV Community

Vhub Systems
Vhub Systems

Posted on

Playwright Stealth Mode in 2026: The 7 Patches That Actually Matter

The playwright-stealth npm package hasn't been updated in over a year. If you're using it unchanged, you're leaving the most important detections unfixed.

Here are the 7 browser fingerprint patches that matter in 2026, with working code.

Why playwright-stealth Falls Short

The original playwright-stealth package patches ~12 JavaScript properties. Modern anti-bot systems check 40+. The most common failure modes in 2026:

  1. WebGL fingerprint not patched → GPU mismatch
  2. navigator.permissions not patched → notification permission query returns wrong value
  3. window.chrome not complete → missing loadTimes() and csi() methods
  4. HTTP/2 fingerprint unchanged → TLS JA3 leaks Python/Node origin
  5. iframe detection not handled → nested iframes expose headless context

The 7 Patches

Patch 1: webdriver

await page.addInitScript(() => {
  Object.defineProperty(navigator, 'webdriver', {
    get: () => undefined,
    configurable: true
  });
});
Enter fullscreen mode Exit fullscreen mode

Note: undefined not false. Some detectors specifically check for false as a patched value.


Patch 2: plugins and mimeTypes

await page.addInitScript(() => {
  const plugins = [
    { name: 'PDF Viewer', description: "'Portable Document Format', filename: 'internal-pdf-viewer' },"
    { name: 'Chrome PDF Viewer', description: "'', filename: 'internal-pdf-viewer' },"
    { name: 'Chromium PDF Viewer', description: "'', filename: 'internal-pdf-viewer' },"
    { name: 'Microsoft Edge PDF Viewer', description: "'', filename: 'internal-pdf-viewer' },"
    { name: 'WebKit built-in PDF', description: "'', filename: 'internal-pdf-viewer' }"
  ];

  Object.defineProperty(navigator, 'plugins', {
    get: () => Object.assign(plugins, { item: i => plugins[i], namedItem: n => plugins.find(p => p.name === n), refresh: () => {} }),
    configurable: true
  });

  Object.defineProperty(navigator, 'mimeTypes', {
    get: () => ({ length: 2, item: i => null, namedItem: n => null }),
    configurable: true
  });
});
Enter fullscreen mode Exit fullscreen mode

Patch 3: window.chrome (the most commonly incomplete patch)

await page.addInitScript(() => {
  if (!window.chrome) window.chrome = {};

  window.chrome.app = {
    isInstalled: false,
    InstallState: { DISABLED: 'disabled', INSTALLED: 'installed', NOT_INSTALLED: 'not_installed' },
    RunningState: { CANNOT_RUN: 'cannot_run', READY_TO_RUN: 'ready_to_run', RUNNING: 'running' }
  };

  window.chrome.runtime = {
    OnInstalledReason: { CHROME_UPDATE: 'chrome_update', INSTALL: 'install', SHARED_MODULE_UPDATE: 'shared_module_update', UPDATE: 'update' },
    OnRestartRequiredReason: { APP_UPDATE: 'app_update', OS_UPDATE: 'os_update', PERIODIC: 'periodic' },
    PlatformArch: { ARM: 'arm', ARM64: 'arm64', MIPS: 'mips', MIPS64: 'mips64', X86_32: 'x86-32', X86_64: 'x86-64' },
    PlatformOs: { ANDROID: 'android', CROS: 'cros', LINUX: 'linux', MAC: 'mac', OPENBSD: 'openbsd', WIN: 'win' },
    RequestUpdateCheckStatus: { NO_UPDATE: 'no_update', THROTTLED: 'throttled', UPDATE_AVAILABLE: 'update_available' },
    id: undefined,
    connect: () => {},
    sendMessage: () => {}
  };

  // Required: loadTimes and csi (missing from most stealth libraries)
  window.chrome.loadTimes = function() {
    return {
      requestTime: Date.now() / 1000,
      startLoadTime: Date.now() / 1000,
      commitLoadTime: Date.now() / 1000,
      finishDocumentLoadTime: 0,
      finishLoadTime: 0,
      firstPaintTime: 0,
      firstPaintAfterLoadTime: 0,
      navigationType: 'Other',
      wasFetchedViaSpdy: false,
      wasNpnNegotiated: false,
      npnNegotiatedProtocol: 'unknown',
      wasAlternateProtocolAvailable: false,
      connectionInfo: 'http/1.1'
    };
  };

  window.chrome.csi = function() {
    return {
      startE: Date.now(),
      onloadT: Date.now(),
      pageT: 3000 + Math.random() * 1000,
      tran: 15
    };
  };
});
Enter fullscreen mode Exit fullscreen mode

Patch 4: navigator.permissions

await page.addInitScript(() => {
  const originalQuery = window.navigator.permissions.query;
  window.navigator.permissions.query = (parameters) => (
    parameters.name === 'notifications'
      ? Promise.resolve({ state: Notification.permission })
      : originalQuery(parameters)
  );
});
Enter fullscreen mode Exit fullscreen mode

Patch 5: WebGL vendor/renderer

await page.addInitScript(() => {
  const getParameter = WebGLRenderingContext.prototype.getParameter;
  WebGLRenderingContext.prototype.getParameter = function(parameter) {
    if (parameter === 37445) return 'Intel Inc.'; // UNMASKED_VENDOR_WEBGL
    if (parameter === 37446) return 'Intel Iris OpenGL Engine'; // UNMASKED_RENDERER_WEBGL
    return getParameter.call(this, parameter);
  };
});
Enter fullscreen mode Exit fullscreen mode

Replace with GPU values that match your target audience (Intel for consumer laptops, NVIDIA for power users).


Patch 6: languages and locale

await page.addInitScript(() => {
  Object.defineProperty(navigator, 'language', { get: () => 'en-US' });
  Object.defineProperty(navigator, 'languages', { get: () => ['en-US', 'en'] });
});
Enter fullscreen mode Exit fullscreen mode

Set to match your proxy location. German proxy + English language = mismatch signal.


Patch 7: iframe contentWindow isolation

await page.addInitScript(() => {
  // Prevent iframe-based detection of patched properties
  const origCreateElement = document.createElement.bind(document);
  document.createElement = function(...args) {
    const element = origCreateElement(...args);
    if (args[0].toLowerCase() === 'iframe') {
      Object.defineProperty(element, 'contentWindow', {
        get: function() {
          const win = this.contentWindow;
          if (win) {
            Object.defineProperty(win.navigator, 'webdriver', { get: () => undefined });
          }
          return win;
        }
      });
    }
    return element;
  };
});
Enter fullscreen mode Exit fullscreen mode

Combining Into a Utility Function

async function applyStealthPatches(page) {
  await page.addInitScript(() => {
    // Patch 1: webdriver
    Object.defineProperty(navigator, 'webdriver', { get: () => undefined, configurable: true });
    // ... (all patches above)
  });
}

// Usage
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({
  userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
  viewport: { width: 1920, height: 1080 }
});
const page = await context.newPage();
await applyStealthPatches(page);
Enter fullscreen mode Exit fullscreen mode

When Patches Aren't Enough

For sites using Cloudflare Turnstile or enterprise-grade systems (DataDome, Kasada), even perfect browser patching isn't enough — the TLS fingerprint at the network level still exposes Node.js/Playwright.

At that point, the options are:

  1. Playwright + residential proxies + CAPTCHA services (~$3-8/1K requests)
  2. Pre-built cloud actors that handle the whole stack

For the most common scraping targets (LinkedIn, Amazon, Google Maps, Twitter), the Apify Scrapers Bundle ($29) includes actors pre-configured with stealth settings for each site — saving you the debugging cycle.


Which anti-bot system are you trying to work around? Drop a comment with the domain — I'll tell you whether patches are enough or if you need a different approach.

n8n AI Automation Pack ($39) — 5 production-ready workflows

Related Tools

Top comments (0)