Skip to content

feat(browse): comprehensive anti-bot stealth patches#1112

Open
garrytan wants to merge 4 commits into
mainfrom
feature/stealth-patches
Open

feat(browse): comprehensive anti-bot stealth patches#1112
garrytan wants to merge 4 commits into
mainfrom
feature/stealth-patches

Conversation

@garrytan
Copy link
Copy Markdown
Owner

What

New stealth.ts module with comprehensive anti-bot detection countermeasures for GStack Browser. Replaces the inline patches in browser-manager.ts with a shared module used by both headless and headed launch paths.

Why

The existing stealth patches missed several critical detection vectors:

Detection Vector Before After
webdriver in navigator 🚨 true (property exists) false (deleted from prototype)
WebGL renderer 🚨 SwiftShader Apple M1 Pro, OpenGL 4.1
Plugins instanceof PluginArray 🚨 failed (raw array) passed (proper PluginArray)
chrome.app 🚨 missing ✅ present with correct shape
WebDriver (SannySoft test) 🚨 present (failed) missing (passed)
mediaDevices 🚨 missing ✅ present

Before: SannySoft flagged 3 vectors. After: 100% pass rate.

Detection Vectors Addressed

  1. navigator.webdriver property existence — not just the value, the property itself must be deleted from Navigator.prototype. Bot detectors check "webdriver" in navigator.
  2. WebGL renderer spoofing — SwiftShader (software GPU in containers) is the docs: add README and CLAUDE.md #1 giveaway. Spoofed to Apple M1 Pro.
  3. Proper PluginArray — real Chrome plugins with instanceof PluginArray passing, MimeType objects, namedItem() method.
  4. Complete chrome objectchrome.app, chrome.runtime, chrome.loadTimes(), chrome.csi() with correct shapes.
  5. CDP artifact cleanup — removes cdc_*, $cdc_*, __webdriver* properties injected by Chrome DevTools Protocol.
  6. Permissions API — returns prompt for notifications (automation browsers return denied).
  7. Media devices — fakes navigator.mediaDevices in environments that lack them.
  8. Function.toString() protection — overridden functions return [native code] to avoid detection.

Changes

  • New: browse/src/stealth.ts — shared stealth module with stealthArgs and applyStealthPatches()
  • Modified: browse/src/browser-manager.ts — imports and uses new module in both launch() and launchHeaded(), removing 56 lines of inline patches

Tested Against

✅ NYT, LinkedIn, Google Search, Bloomberg, BleepingComputer, Brave Search, DuckDuckGo

Remaining hard targets (Reddit, FT, WSJ) are blocked by IP reputation checks beyond browser fingerprinting — not a fingerprint issue.

How to Test

  1. bun run server → launch browser
  2. Navigate to bot.sannysoft.com → all tests should pass
  3. Navigate to nytimes.com → should load without CAPTCHA
  4. Navigate to linkedin.com/in/garrytan → should load profile (not login wall)

Add stealth.ts module that addresses all known automation fingerprints:

1. navigator.webdriver property deletion (not just value override) -
   bot detectors check property existence via 'webdriver' in navigator
2. WebGL renderer spoofing (SwiftShader → Apple M1 Pro) - SwiftShader
   is the #1 giveaway of container/headless environments
3. Proper PluginArray that passes instanceof checks - raw arrays fail
   PluginArray instanceof which DataDome/Cloudflare check
4. Complete chrome object (app, runtime, loadTimes, csi) - shallow
   stubs missing chrome.app get flagged
5. CDP runtime artifact cleanup (cdc_*, $cdc_*, __webdriver*)
6. Permissions API normalization (prompt, not denied)
7. Media devices presence for containers
8. Function.toString() protection - overridden functions look native

Passes SannySoft (bot.sannysoft.com) 100%. Replaces inline patches in
browser-manager.ts with shared module used by both headless launch()
and headed launchHeaded() paths.

Tested against: NYT, LinkedIn, Google, Bloomberg, BleepingComputer,
Brave Search, DuckDuckGo - all previously blocked from automation
browsers, all now pass through.

Remaining hard targets (Reddit, FT, WSJ) blocked by IP reputation
checks beyond browser fingerprinting.
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 21, 2026

E2E Evals: ✅ PASS

8/8 tests passed | $1.19 total cost | 12 parallel runners

Suite Result Status Cost
e2e-browse 2/2 $0.13
e2e-deploy 2/2 $0.3
e2e-qa-workflow 1/1 $0.44
llm-judge 1/1 $0.02
e2e-deploy 2/2 $0.3

12x ubicloud-standard-2 (Docker: pre-baked toolchain + deps) | wall clock ≈ slowest suite

gstack added 3 commits April 21, 2026 03:06
Tests (52 total, 0 failures):

Unit tests (33):
- Module exports validation (stealthArgs shape, applyStealthPatches type)
- Launch args content (AutomationControlled, no-first-run, no forbidden flags)
- Init script source analysis (all 10 patch vectors verified present)
- applyStealthPatches API (mock context, GPU args, serialization, idempotency)
- Adversarial edge cases (array spread safety, extension compat, GPU plausibility)
- Import integration (browser-manager.ts correctly imports and calls both paths)
- Old inline patches removal verification

E2E tests (19):
- Real Chromium launch with stealth patches applied
- navigator.webdriver value AND property existence
- WebGL1 + WebGL2 renderer spoofing
- PluginArray instanceof + shape verification
- Complete chrome object (app, runtime, loadTimes, csi)
- Languages, permissions, CDP artifacts, Playwright globals
- Platform/UA consistency
- Patches survive page navigation

Bug fix: navigator.platform now spoofed to 'MacIntel' when UA claims
Macintosh. Previously reported 'Linux x86_64' in containers, which
contradicts the Mac user agent and is a detectable fingerprint mismatch.
Caught by the e2e test.
1. HIGH — Function.toString Map exfiltration:
   Replaced Map with WeakMap + bound methods. A malicious page could
   monkeypatch Map.prototype.has to capture the override store, then
   use it to cloak malicious functions as [native code]. WeakMap with
   pre-bound has/get methods prevents this side-channel.

2. MEDIUM — Static GPU fingerprint:
   Default GPU renderer now randomly selects from 5 common Apple chip
   variants (M1, M1 Pro, M1 Max, M2, M3) per session. Prevents sites
   from building a static GStack-specific fingerprint signature.

3. Tests updated: 54 total (35 unit + 19 e2e), 0 failures.
   Added tests for WeakMap usage and GPU randomization.
1. HIGH — webdriver fallback logic bug: define-then-delete on instance
   re-exposes prototype getter returning true. Fixed: override getter
   directly on Navigator.prototype when delete fails.

2. HIGH — chrome.runtime clobbered unconditionally, breaking extension
   messaging (content scripts, sidepanel, background). Fixed: only stub
   methods that don't already exist (if !w.chrome.runtime.connect ...).

3. MEDIUM — PermissionStatus shape missing EventTarget behavior. Sites
   calling addEventListener on the result would throw. Fixed: create
   object via Object.create(EventTarget.prototype).

4. MEDIUM — Plugin item()/namedItem() returned undefined instead of null
   for missing entries. Detectable and breaks strict checks. Fixed: ?? null.

5. MEDIUM — WebGL params spoofed even without debug extension, which is
   detectable as synthetic. Fixed: check getExtension() first.

6. LOW/MEDIUM — Only toString itself was registered in the WeakMap;
   patched getParameter was still inspectable. Fixed: register all
   patched prototype functions.

7. LOW — Import from playwright-core instead of playwright (transitive
   dependency). Fixed: import from playwright (direct dependency).

All 129 tests pass (54 stealth + 75 existing).
shelman09 pushed a commit to Namleh-Studios/gstack that referenced this pull request May 19, 2026
…tches (garrytan#1112)

Rebases @garrytan's PR garrytan#1112 (Apr 2026, abandoned) onto the current
browse/src/stealth.ts contract. The existing minimal "codex narrowed"
stealth (webdriver-mask + AutomationControlled launch arg) stays the
default. PR garrytan#1112's six additional patches are added behind an opt-in
GSTACK_STEALTH=extended env flag.

Extended-mode patches (applied AFTER the default mask, in order):
  1. delete navigator.webdriver from prototype (not just the getter —
     detectors check `"webdriver" in navigator`)
  2. WebGL renderer spoof to Apple M1 Pro (SwiftShader was the garrytan#1
     software-GPU tell in containers)
  3. navigator.plugins returns a PluginArray-prototype-passing array
     with MimeType objects and namedItem()
  4. window.chrome populated with chrome.app, chrome.runtime,
     chrome.loadTimes(), chrome.csi() with realistic shapes
  5. navigator.mediaDevices backfilled when headless drops it
  6. CDP cdc_*-prefixed window globals cleared

Why opt-in: the default mode's contract is fingerprint CONSISTENCY,
which protects against detectors that flag spoofing mismatch. Extended
mode actively lies about the environment; sites that reflect on these
properties can break. Users who hit detection in default mode can flip
GSTACK_STEALTH=extended for SannySoft 100% pass-rate.

Twenty unit tests pin the env-flag semantics, all six patches' code
presence, and the applyStealth wiring order. Live SannySoft pass-rate
verification stays in the periodic-tier E2E suite.

Contributed by @garrytan via garrytan#1112 (rebased — original PR opened
before the codex-narrowed minimum landed; rebase preserves the
narrowed default while adding the SannySoft-passing path as opt-in).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant