garrytan / garrytan/gstack

browse: Layer C stealth fakes chrome.runtime.connect/sendMessage, sites detect a phantom companion extension and break login

Open Beginner friendly
#2,780 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
133k
Forks
19.9k
Avg merge
18h 46m
Merged PRs (30d)
26

Description

## Summary

The always-on Layer C stealth script (`browse/src/stealth.ts`, `buildStealthScript`) injects a fake `window.chrome.runtime` that includes callable `connect` and `sendMessage` stubs. Real Chrome never exposes those methods to an ordinary web page. Web apps that ship a companion Chrome extension commonly probe `chrome.runtime.sendMessage` to decide whether their extension is installed. In GStack Browser that probe returns a function, so the app concludes the extension is present and routes its auth/session flow through the extension. The flow fails and the user is bounced back to the login page in a loop. The same site logs in fine in stock Chrome, Edge, and incognito.

There is no opt-out: Layer C runs on every context-creation path and only the *extended* mode is gated by `GSTACK_STEALTH`.

## Why this is a fingerprint inconsistency, not just a site bug

In real Chrome, `chrome.runtime` on a web page is:

- **undefined** in the vast majority of cases (no connectable extension), or
- **present with `connect` / `sendMessage`** only when an installed extension declares that origin in `externally_connectable`.

Layer C's shape is a third thing real Chrome never produces: `chrome.runtime` present, with `connect` / `sendMessage` that throw `TypeError: Error in invocation of runtime.sendMessage: No matching signature.` for any argument. v1.28's own rationale (PR #1363) was that inconsistent fakes make detection easier, not harder. The enum shape (`OnInstalledReason`, `PlatformOs`, ...) is what the Cloudflare/DataDome presence checks look at, and it can stay.

## Reproduction

In GStack Browser (headed or headless), on any page:

```
$ browse goto https://
$ browse js "JSON.stringify({hasRuntime: !!chrome.runtime, hasSendMessage: typeof chrome.runtime.sendMessage, ua: navigator.userAgent})"
{"hasRuntime":true,"hasSendMessage":"function","ua":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/151.0.7922.34 Safari/537.36"}
```

Same check in stock Chrome / Edge / incognito without a connectable extension:

```
> typeof chrome.runtime
'undefined'
```

Observed effect on a real SaaS login (redacted): login POST succeeds, tenant callback loads, app calls its `isExtensionInstalled()` helper, `chrome.runtime.sendMessage(...)` throws the fake TypeError (visible in `.gstack/browse-console.log`), app then POSTs `/logout` and redirects to `/logout?...&client_name=extension` which returns 403, user lands on the login page again.

## Proposed fix

Remove `connect` and `sendMessage` from the injected `chrome.runtime` object and keep the enum shape. That matches what real Chrome shows a page with no connectable extension, and removes the only callable surface a detector could poke at and get a wrong answer from.

Tests to update: `stealth-layer-c.test.ts` ("ships full enum shape" currently asserts the two methods exist; markNative count drops by 2) and `stealth-webdriver.test.ts` (asserts `runtime.connect()` throws "No matching signature").

PR with the change and updated tests: linked in the comments / PR reference.

## Environment

- gstack v1.79.0.0 (commit 0d1bd56)
- Windows 11, Playwright Chromium 151 (bundled), headed mode via `browse connect`

Contributor guide

Open the contributing guide

Research direction

Start in browse/src/stealth.ts at buildStealthScript, then read stealth-layer-c.test.ts and stealth-webdriver.test.ts for the current runtime assertions. Run those tests first, update expectations for the runtime shape and markNative count, and confirm the fake connect/sendMessage surface is no longer present.

Written by the indexing model from the issue text.

Assessment

Tech stack
playwright, typescript
Domain
devtools
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
82/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.