feat: upgrade to Chromium v145 with Patchright CDP stealth

- Migrate from Playwright to Patchright driver for CDP leak prevention
- Add screen dimension spoofing args (--fingerprint-screen-width/height, --fingerprint-taskbar-height)
- Add realistic default viewport (1920x955) to avoid Playwright detection
- Add color_scheme param to launch_context, add fingerprint scan test
- Update READMEs for v145
This commit is contained in:
CloakHQ
2026-03-02 02:59:15 +01:00
parent 6506b5fe44
commit 0bbc170747
8 changed files with 284 additions and 25 deletions
+22 -16
View File
@@ -20,7 +20,8 @@
Drop-in Playwright/Puppeteer replacement for Python and JavaScript. Same API, same code — just swap the import. Your browser now scores **0.9 on reCAPTCHA v3**, passes **Cloudflare Turnstile**, and clears **30 out of 30** stealth detection tests. Drop-in Playwright/Puppeteer replacement for Python and JavaScript. Same API, same code — just swap the import. Your browser now scores **0.9 on reCAPTCHA v3**, passes **Cloudflare Turnstile**, and clears **30 out of 30** stealth detection tests.
- 🔒 **16 source-level C++ patches** — not JS injection, not config flags - 🔒 **22 source-level C++ patches** — not JS injection, not config flags
- 🛡️ **CDP stealth built-in** — powered by [Patchright](https://github.com/Kaliiiiiiiiii-Vinyzu/patchright), hides Playwright's automation signals
- 🎯 **0.9 reCAPTCHA v3 score** — human-level, server-verified - 🎯 **0.9 reCAPTCHA v3 score** — human-level, server-verified
- ☁️ **Passes Cloudflare Turnstile**, FingerprintJS, BrowserScan — 30/30 tests - ☁️ **Passes Cloudflare Turnstile**, FingerprintJS, BrowserScan — 30/30 tests
- 🔄 **Drop-in replacement** — works with Playwright (Python & JS) and Puppeteer (JS) - 🔄 **Drop-in replacement** — works with Playwright (Python & JS) and Puppeteer (JS)
@@ -79,11 +80,12 @@ On first run, the stealth Chromium binary is automatically downloaded (~200MB, c
- **Config-level patches break** — `playwright-stealth`, `undetected-chromedriver`, and `puppeteer-extra` inject JavaScript or tweak flags. Every Chrome update breaks them. Antibot systems detect the patches themselves. - **Config-level patches break** — `playwright-stealth`, `undetected-chromedriver`, and `puppeteer-extra` inject JavaScript or tweak flags. Every Chrome update breaks them. Antibot systems detect the patches themselves.
- **CloakBrowser patches Chromium source code** — fingerprints are modified at the C++ level, compiled into the binary. Detection sites see a real browser because it *is* a real browser. - **CloakBrowser patches Chromium source code** — fingerprints are modified at the C++ level, compiled into the binary. Detection sites see a real browser because it *is* a real browser.
- **Two layers of stealth** — C++ patches handle fingerprints (GPU, screen, UA, voices, media devices), while the Patchright driver eliminates CDP automation leaks (Runtime.enable, Chrome flags, console detection). Most stealth tools only do one or the other.
- **One line to switch** — same Playwright API, no new abstractions, no CAPTCHA-solving services. - **One line to switch** — same Playwright API, no new abstractions, no CAPTCHA-solving services.
## Test Results ## Test Results
All tests verified against live detection services. Last tested: Feb 2026 (Chromium 142). All tests verified against live detection services. Last tested: Feb 2026 (Chromium 145).
| Detection Service | Stock Playwright | CloakBrowser | Notes | | Detection Service | Stock Playwright | CloakBrowser | Notes |
|---|---|---|---| |---|---|---|---|
@@ -98,7 +100,7 @@ All tests verified against live detection services. Last tested: Feb 2026 (Chrom
| `navigator.webdriver` | `true` | **`false`** | Source-level patch | | `navigator.webdriver` | `true` | **`false`** | Source-level patch |
| `navigator.plugins.length` | 0 | **5** | Real plugin list | | `navigator.plugins.length` | 0 | **5** | Real plugin list |
| `window.chrome` | `undefined` | **`object`** | Present like real Chrome | | `window.chrome` | `undefined` | **`object`** | Present like real Chrome |
| UA string | `HeadlessChrome` | **`Chrome/142.0.0.0`** | No headless leak | | UA string | `HeadlessChrome` | **`Chrome/145.0.0.0`** | No headless leak |
| CDP detection | Detected | **Not detected** | `isAutomatedWithCDP: false` | | CDP detection | Detected | **Not detected** | `isAutomatedWithCDP: false` |
| TLS fingerprint | Mismatch | **Identical to Chrome** | ja3n/ja4/akamai match | | TLS fingerprint | Mismatch | **Identical to Chrome** | ja3n/ja4/akamai match |
@@ -140,17 +142,7 @@ CloakBrowser is a thin wrapper (Python + JavaScript) around a custom-built Chrom
3. **Every launch** → Playwright or Puppeteer starts with our binary + stealth args 3. **Every launch** → Playwright or Puppeteer starts with our binary + stealth args
4. **You write code** → standard Playwright/Puppeteer API, nothing new to learn 4. **You write code** → standard Playwright/Puppeteer API, nothing new to learn
The binary includes 16 source-level patches that modify: The binary includes 26 source-level patches covering canvas, WebGL, audio, fonts, GPU, screen properties, hardware reporting, and automation signal removal.
- Canvas fingerprint generation
- WebGL renderer output
- Audio processing fingerprint
- Font enumeration results
- Hardware concurrency reporting
- Client rect measurements
- GPU vendor/renderer strings
- WebDriver flag
- Headless detection signals
- And more...
These are compiled into the Chromium binary — not injected via JavaScript, not set via flags. These are compiled into the Chromium binary — not injected via JavaScript, not set via flags.
@@ -462,7 +454,7 @@ page.goto("https://example.com")
| Linux x64 binary | ✅ Released | | Linux x64 binary | ✅ Released |
| macOS arm64 (Apple Silicon) | ✅ Released | | macOS arm64 (Apple Silicon) | ✅ Released |
| macOS x64 (Intel) | ✅ Released | | macOS x64 (Intel) | ✅ Released |
| Chromium 145 build | 🔜 In progress | | Chromium 145 build | ✅ Released |
| JavaScript/Puppeteer + Playwright support | ✅ Released | | JavaScript/Puppeteer + Playwright support | ✅ Released |
| Fingerprint rotation per session | ✅ Released | | Fingerprint rotation per session | ✅ Released |
| Built-in proxy rotation | 📋 Planned | | Built-in proxy rotation | 📋 Planned |
@@ -523,6 +515,20 @@ This runs a real headed browser rendered on a virtual display — no physical mo
## Troubleshooting ## Troubleshooting
**Reddit or similar sites show CAPTCHA / "Prove your humanity"**
Some sites (notably Reddit homepage) use HTTP/2 fingerprinting that detects Playwright's connection layer. Pass `--disable-http2` to fall back to HTTP/1.1:
```python
browser = launch(args=["--disable-http2"])
```
```javascript
const browser = await launch({ args: ['--disable-http2'] });
```
Only use this flag for sites that require it — most sites work fine with HTTP/2.
**Binary download fails / timeout** **Binary download fails / timeout**
Set a custom download URL or use a local binary: Set a custom download URL or use a local binary:
```bash ```bash
@@ -532,7 +538,7 @@ export CLOAKBROWSER_BINARY_PATH=/path/to/your/chrome
**"playwright install" vs CloakBrowser binary** **"playwright install" vs CloakBrowser binary**
You do NOT need `playwright install chromium`. CloakBrowser downloads its own binary. You only need Playwright's system deps: You do NOT need `playwright install chromium`. CloakBrowser downloads its own binary. You only need Playwright's system deps:
```bash ```bash
playwright install-deps chromium patchright install-deps chromium
``` ```
**reCAPTCHA v3 scores are low (0.10.3)** **reCAPTCHA v3 scores are low (0.10.3)**
+10 -5
View File
@@ -18,7 +18,7 @@ import logging
from typing import Any from typing import Any
from urllib.parse import unquote, urlparse, urlunparse from urllib.parse import unquote, urlparse, urlunparse
from .config import get_default_stealth_args from .config import DEFAULT_VIEWPORT, get_default_stealth_args
from .download import ensure_binary from .download import ensure_binary
logger = logging.getLogger("cloakbrowser") logger = logging.getLogger("cloakbrowser")
@@ -61,7 +61,7 @@ def launch(
>>> print(page.title()) >>> print(page.title())
>>> browser.close() >>> browser.close()
""" """
from playwright.sync_api import sync_playwright from patchright.sync_api import sync_playwright
binary_path = ensure_binary() binary_path = ensure_binary()
timezone, locale = _maybe_resolve_geoip(geoip, proxy, timezone, locale) timezone, locale = _maybe_resolve_geoip(geoip, proxy, timezone, locale)
@@ -129,7 +129,7 @@ async def launch_async(
>>> >>>
>>> asyncio.run(main()) >>> asyncio.run(main())
""" """
from playwright.async_api import async_playwright from patchright.async_api import async_playwright
binary_path = ensure_binary() binary_path = ensure_binary()
timezone, locale = _maybe_resolve_geoip(geoip, proxy, timezone, locale) timezone, locale = _maybe_resolve_geoip(geoip, proxy, timezone, locale)
@@ -168,6 +168,7 @@ def launch_context(
viewport: dict | None = None, viewport: dict | None = None,
locale: str | None = None, locale: str | None = None,
timezone_id: str | None = None, timezone_id: str | None = None,
color_scheme: str | None = None,
geoip: bool = False, geoip: bool = False,
**kwargs: Any, **kwargs: Any,
) -> Any: ) -> Any:
@@ -185,6 +186,9 @@ def launch_context(
viewport: Viewport size dict, e.g. {"width": 1920, "height": 1080}. viewport: Viewport size dict, e.g. {"width": 1920, "height": 1080}.
locale: Browser locale, e.g. "en-US". locale: Browser locale, e.g. "en-US".
timezone_id: Timezone, e.g. "America/New_York". timezone_id: Timezone, e.g. "America/New_York".
color_scheme: Color scheme preference 'light', 'dark', or 'no-preference'.
Default: None (uses Chromium default, which is 'light').
Note: 'no-preference' doesn't work in Patchright (falls back to 'light').
geoip: Auto-detect timezone/locale from proxy IP (default False). geoip: Auto-detect timezone/locale from proxy IP (default False).
**kwargs: Passed to browser.new_context(). **kwargs: Passed to browser.new_context().
@@ -200,12 +204,13 @@ def launch_context(
context_kwargs: dict[str, Any] = {} context_kwargs: dict[str, Any] = {}
if user_agent: if user_agent:
context_kwargs["user_agent"] = user_agent context_kwargs["user_agent"] = user_agent
if viewport: context_kwargs["viewport"] = viewport or DEFAULT_VIEWPORT
context_kwargs["viewport"] = viewport
if locale: if locale:
context_kwargs["locale"] = locale context_kwargs["locale"] = locale
if timezone_id: if timezone_id:
context_kwargs["timezone_id"] = timezone_id context_kwargs["timezone_id"] = timezone_id
if color_scheme:
context_kwargs["color_scheme"] = color_scheme
context_kwargs.update(kwargs) context_kwargs.update(kwargs)
try: try:
+12
View File
@@ -45,8 +45,20 @@ def get_default_stealth_args() -> list[str]:
"--fingerprint-hardware-concurrency=8", "--fingerprint-hardware-concurrency=8",
"--fingerprint-gpu-vendor=NVIDIA Corporation", "--fingerprint-gpu-vendor=NVIDIA Corporation",
"--fingerprint-gpu-renderer=NVIDIA GeForce RTX 3070", "--fingerprint-gpu-renderer=NVIDIA GeForce RTX 3070",
"--fingerprint-taskbar-height=40",
"--fingerprint-screen-width=1920",
"--fingerprint-screen-height=1080",
"--window-size=1920,1080",
] ]
# ---------------------------------------------------------------------------
# Default viewport — realistic maximized Chrome on 1080p Windows
# screen=1920x1080, availHeight=1040 (minus 40px taskbar),
# innerHeight=955 (minus ~85px Chrome UI: tabs + address bar + bookmarks)
# ---------------------------------------------------------------------------
DEFAULT_VIEWPORT = {"width": 1920, "height": 955}
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Platform detection # Platform detection
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
+227
View File
@@ -0,0 +1,227 @@
"""Test against fingerprint-scan.com and CreepJS.
Tests the specific headless detection signals flagged by the community:
- noTaskbar, noContentIndex, noContactsManager, noDownlinkMax
- Bot risk score (fingerprint-scan.com)
- Headless/stealth percentages (CreepJS)
- Full CreepJS signal breakdown (likeHeadless, headless, stealth)
Usage:
python examples/fingerprint_scan_test.py
python examples/fingerprint_scan_test.py --proxy http://10.50.96.5:8888
python examples/fingerprint_scan_test.py --headless
"""
import sys
from cloakbrowser import launch_context
HEADLESS = "--headless" in sys.argv
PROXY = None
for i, arg in enumerate(sys.argv):
if arg == "--proxy" and i + 1 < len(sys.argv):
PROXY = sys.argv[i + 1]
def test_fingerprint_scan(page):
"""fingerprint-scan.com — bot risk score + headless detection signals."""
print("=== fingerprint-scan.com ===")
page.goto("https://fingerprint-scan.com/", wait_until="domcontentloaded", timeout=30000)
page.wait_for_timeout(20000) # Castle.js needs time to compute score
# Check bot risk score
score = page.evaluate(
'document.getElementById("fingerprintScore")?.textContent || "Score not rendered"'
)
print(f"Bot Risk Score: {score}")
# Check headless detection signals
apis = page.evaluate("""() => ({
noTaskbar: screen.height === screen.availHeight,
taskbarSize: screen.height - screen.availHeight,
noContentIndex: typeof window.ContentIndex === "undefined",
noContactsManager: !("contacts" in navigator),
noDownlinkMax: !("downlinkMax" in (navigator.connection || {})),
downlinkMax: navigator.connection?.downlinkMax ?? null,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
webdriver: navigator.webdriver,
isPlaywright: "__pwInitScripts" in window || "__playwright__binding__" in window,
webgpu: typeof navigator.gpu !== "undefined" ? "available" : "NOT_AVAILABLE",
scrollbarWidth: (() => { const d = document.createElement("div"); d.style.cssText = "overflow:scroll;width:100px;height:100px;position:absolute;top:-999px"; document.body.appendChild(d); const w = d.offsetWidth - d.clientWidth; d.remove(); return w; })()
})""")
print("\nHeadless detection signals:")
headless_fails = 0
for k, v in apis.items():
is_fail = k.startswith("no") and v is True
if is_fail:
headless_fails += 1
flag = "FAIL" if is_fail else ""
print(f" {k}: {v} {flag}")
# Extract bot test results from page
bot_tests = page.evaluate("""() => {
const text = document.body.innerText;
const tests = {};
for (const key of ['WebDriver', 'Is Selenium Chrome', 'CDP Check', 'Is Playwright']) {
const match = text.match(new RegExp(key + '\\\\s+(true|false)'));
if (match) tests[key] = match[1];
}
return tests;
}""")
print("\nBot Detection Tests:")
for k, v in bot_tests.items():
status = "PASS" if v == "false" else "FAIL"
print(f" {k}: {v} [{status}]")
page.screenshot(path="/results/fingerprint-scan.png", full_page=True)
print("\nScreenshot: /results/fingerprint-scan.png")
return {
"score": score,
"headless_fails": headless_fails,
"apis": apis,
"bot_tests": bot_tests,
}
def test_creepjs(page):
"""abrahamjuliot.github.io/creepjs — comprehensive fingerprint analysis."""
print("\n=== CreepJS ===")
page.goto(
"https://abrahamjuliot.github.io/creepjs/", wait_until="domcontentloaded", timeout=30000
)
print("Waiting 30s for CreepJS analysis...")
page.wait_for_timeout(30000)
# Extract % scores from page text (matches test-infra/matrix_tests/group3_bot_detection.py)
scores = page.evaluate("""() => {
const text = document.body.innerText;
const likeMatch = text.match(/(\\d+)%\\s*like headless/i);
const headlessMatch = text.match(/(\\d+)%\\s*headless:/i);
const stealthMatch = text.match(/(\\d+)%\\s*stealth:/i);
return {
likeHeadlessPct: likeMatch ? parseInt(likeMatch[1]) : null,
headlessPct: headlessMatch ? parseInt(headlessMatch[1]) : null,
stealthPct: stealthMatch ? parseInt(stealthMatch[1]) : null,
};
}""")
print(f"\nScores:")
print(f" like-headless: {scores['likeHeadlessPct']}% (target: <=30%)")
print(f" headless: {scores['headlessPct']}% (target: 0%)")
print(f" stealth: {scores['stealthPct']}% (target: 0%)")
# Extract full signal breakdown from window.Fingerprint.headless (CreepJS internal object)
signals = page.evaluate("""() => {
try {
const fp = window.Fingerprint;
if (!fp || !fp.headless) return null;
return {
likeHeadless: fp.headless.likeHeadless || null,
headless: fp.headless.headless || null,
stealth: fp.headless.stealth || null,
};
} catch { return null; }
}""")
if signals:
if signals.get("likeHeadless"):
print("\nlikeHeadless signals:")
fails = 0
for k, v in signals["likeHeadless"].items():
is_fail = v is True
if is_fail:
fails += 1
flag = " FAIL" if is_fail else ""
print(f" {k}: {v}{flag}")
print(f" ({fails} fails)")
if signals.get("headless"):
print("\nheadless signals:")
for k, v in signals["headless"].items():
flag = " FAIL" if v is True else ""
print(f" {k}: {v}{flag}")
if signals.get("stealth"):
print("\nstealth signals:")
for k, v in signals["stealth"].items():
flag = " FAIL" if v is True else ""
print(f" {k}: {v}{flag}")
else:
print("\n(window.Fingerprint.headless not available — signals not extracted)")
# Extract platform estimate
platform = page.evaluate("""() => {
try {
const fp = window.Fingerprint;
if (!fp || !fp.platformEstimate) return null;
return fp.platformEstimate;
} catch { return null; }
}""")
if platform:
print(f"\nPlatform estimate: {platform}")
passed = (
scores["headlessPct"] is not None
and scores["headlessPct"] <= 30
and scores["stealthPct"] is not None
and scores["stealthPct"] <= 30
)
print(f"\nVerdict: {'PASS' if passed else 'FAIL'} (<=30% headless, <=30% stealth)")
page.screenshot(path="/results/creepjs.png", full_page=True)
print("Screenshot: /results/creepjs.png")
return {**scores, "signals": signals, "platform": platform}
def main():
print("=" * 60)
print("CloakBrowser — Fingerprint & Headless Detection Tests")
print("=" * 60)
print(f"Mode: {'headless' if HEADLESS else 'headed'}")
print(f"Proxy: {PROXY or 'none'}")
print()
context = launch_context(
headless=HEADLESS,
proxy=PROXY,
args=[
"--fingerprint-screen-width=1920",
"--fingerprint-screen-height=1080",
"--timezone=Asia/Jerusalem",
],
)
page = context.new_page()
try:
fp_result = test_fingerprint_scan(page)
creep_result = test_creepjs(page)
finally:
context.close()
# Summary
print("\n" + "=" * 60)
print("SUMMARY")
print("=" * 60)
print(f"fingerprint-scan.com: {fp_result['score']}")
print(f" Headless signal fails: {fp_result['headless_fails']}")
like = creep_result["likeHeadlessPct"]
headless = creep_result["headlessPct"]
stealth = creep_result["stealthPct"]
print(f"CreepJS: like-headless={like}%, headless={headless}%, stealth={stealth}%")
# Count CreepJS signal fails
sigs = creep_result.get("signals")
if sigs and sigs.get("likeHeadless"):
fail_names = [k for k, v in sigs["likeHeadless"].items() if v is True]
if fail_names:
print(f" likeHeadless fails: {', '.join(fail_names)}")
print("=" * 60)
return 0
if __name__ == "__main__":
sys.exit(main())
+1 -1
View File
@@ -11,7 +11,7 @@
Drop-in Playwright/Puppeteer replacement. Same API — just swap the import. Scores **0.9 on reCAPTCHA v3**, passes **Cloudflare Turnstile**, and clears **30/30** stealth detection tests. Drop-in Playwright/Puppeteer replacement. Same API — just swap the import. Scores **0.9 on reCAPTCHA v3**, passes **Cloudflare Turnstile**, and clears **30/30** stealth detection tests.
- 🔒 **16 source-level C++ patches** — not JS injection, not config flags - 🔒 **22 source-level C++ patches** — not JS injection, not config flags
- 🎯 **0.9 reCAPTCHA v3 score** — human-level, server-verified - 🎯 **0.9 reCAPTCHA v3 score** — human-level, server-verified
- ☁️ **Passes Cloudflare Turnstile**, FingerprintJS, BrowserScan — 30/30 tests - ☁️ **Passes Cloudflare Turnstile**, FingerprintJS, BrowserScan — 30/30 tests
- 🔄 **Drop-in replacement** — works with both Playwright and Puppeteer - 🔄 **Drop-in replacement** — works with both Playwright and Puppeteer
+9
View File
@@ -139,6 +139,11 @@ export function getLocalBinaryOverride(): string | undefined {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Default stealth arguments // Default stealth arguments
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Default viewport — realistic maximized Chrome on 1080p Windows
// screen=1920x1080, availHeight=1040 (minus 40px taskbar),
// innerHeight=955 (minus ~85px Chrome UI: tabs + address bar + bookmarks)
export const DEFAULT_VIEWPORT = { width: 1920, height: 955 };
export function getDefaultStealthArgs(): string[] { export function getDefaultStealthArgs(): string[] {
const seed = Math.floor(Math.random() * 90000) + 10000; // 10000-99999 const seed = Math.floor(Math.random() * 90000) + 10000; // 10000-99999
const isMac = process.platform === "darwin"; const isMac = process.platform === "darwin";
@@ -161,5 +166,9 @@ export function getDefaultStealthArgs(): string[] {
"--fingerprint-hardware-concurrency=8", "--fingerprint-hardware-concurrency=8",
"--fingerprint-gpu-vendor=NVIDIA Corporation", "--fingerprint-gpu-vendor=NVIDIA Corporation",
"--fingerprint-gpu-renderer=NVIDIA GeForce RTX 3070", "--fingerprint-gpu-renderer=NVIDIA GeForce RTX 3070",
"--fingerprint-taskbar-height=40",
"--fingerprint-screen-width=1920",
"--fingerprint-screen-height=1080",
"--window-size=1920,1080",
]; ];
} }
+2 -2
View File
@@ -5,7 +5,7 @@
import type { Browser, BrowserContext } from "playwright-core"; import type { Browser, BrowserContext } from "playwright-core";
import type { LaunchOptions, LaunchContextOptions } from "./types.js"; import type { LaunchOptions, LaunchContextOptions } from "./types.js";
import { getDefaultStealthArgs } from "./config.js"; import { DEFAULT_VIEWPORT, getDefaultStealthArgs } from "./config.js";
import { ensureBinary } from "./download.js"; import { ensureBinary } from "./download.js";
import { parseProxyUrl } from "./proxy.js"; import { parseProxyUrl } from "./proxy.js";
@@ -68,7 +68,7 @@ export async function launchContext(
try { try {
context = await browser.newContext({ context = await browser.newContext({
...(options.userAgent ? { userAgent: options.userAgent } : {}), ...(options.userAgent ? { userAgent: options.userAgent } : {}),
...(options.viewport ? { viewport: options.viewport } : {}), viewport: options.viewport ?? DEFAULT_VIEWPORT,
...(resolved.locale ? { locale: resolved.locale } : {}), ...(resolved.locale ? { locale: resolved.locale } : {}),
...(resolved.timezone ? { timezoneId: resolved.timezone } : {}), ...(resolved.timezone ? { timezoneId: resolved.timezone } : {}),
}); });
+1 -1
View File
@@ -42,7 +42,7 @@ classifiers = [
"Topic :: Software Development :: Testing", "Topic :: Software Development :: Testing",
] ]
dependencies = [ dependencies = [
"playwright>=1.40", "patchright>=1.40",
"httpx>=0.24", "httpx>=0.24",
] ]