Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .github/workflows/dependency-audit.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: Dependency Audit

# Defense-in-depth: surface known CVEs in the dependency tree on every PR.
#
# The redaction guard (lib/redact-engine.ts) and the static-grep tripwire
# tests cover OUR code; nothing in CI watched the THIRD-PARTY tree for
# published advisories. This job runs `bun audit` against the committed
# lockfile so a vulnerable transitive dep shows up at review time.
#
# Intentionally NON-BLOCKING (continue-on-error) to start: it reports, it
# does not gate the merge. Flip `continue-on-error` to false once the tree
# is known-clean to make it a hard gate.

on:
pull_request:
branches: [main]
workflow_dispatch:

concurrency:
group: dependency-audit-${{ github.head_ref }}
cancel-in-progress: true

jobs:
audit:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4

- uses: oven-sh/setup-bun@v1
with:
bun-version: latest

- name: bun audit (advisory scan of the committed lockfile)
run: bun audit
continue-on-error: true
36 changes: 36 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,41 @@
# Changelog

## [1.58.4.0] - 2026-06-20

## **The browser daemon stops handing its root token to any extension that asks.**
## **One real privilege-escalation path closed, plus a sweep of dependency CVEs.**

The `/health` endpoint used to return the daemon's root auth token to any caller whose Origin was a `chrome-extension://` URL, regardless of which extension. On a machine with more than one extension installed, a second extension could read that token and then drive every browse-server endpoint. The token is now scoped: when you pin `BROWSE_EXTENSION_ID`, `/health` only releases it to that exact extension origin, the same way the PTY WebSocket gate already works. Leaving the id unset keeps the old behavior, so nothing breaks for existing installs. Alongside that, a `bun audit` of the dependency tree turned up 37 advisories; the direct dependencies we control are patched, and a new CI job now scans the tree on every pull request so the next one shows up at review time instead of in production.

### The numbers that matter

Source: `bun audit` before/after, and `bun test browse/test/server-auth.test.ts`.

| Metric | Before | After |
|---|---|---|
| `/health` token released to | any `chrome-extension://` origin | only the pinned extension id |
| Direct-dependency CVEs | 2 (`@anthropic-ai/sdk`, `diff`) | 0 |
| CI dependency scanning | none | `bun audit` on every PR |
| server-auth regression tests | 38 | 39 (gate now pinned) |

The single most useful number: a second extension on your machine can no longer scrape the root token from `/health` once the extension id is pinned.

### What this means for builders

If you run GStack Browser on a shared or multi-extension machine, set `BROWSE_EXTENSION_ID` to your extension's id and the daemon stops trusting strangers. Everything else keeps working. Run `bun audit` yourself to see the remaining transitive advisories, which live behind pinned upstream packages.

### Itemized changes

#### Fixed
- `/health` now scopes the bootstrap `AUTH_TOKEN` to an exact `chrome-extension://<BROWSE_EXTENSION_ID>` match when the id is pinned, closing a multi-extension privilege-escalation path. Behavior is unchanged when the id is unset.

#### Changed
- Bumped `@anthropic-ai/sdk` (0.78.0 → 0.91.1, insecure default file permissions) and `diff` (7 → 8, parsePatch DoS), and refreshed transitive dependencies within their ranges.

#### For contributors
- New non-blocking `dependency-audit` GitHub workflow runs `bun audit` on every pull request.
- Added a regression test pinning the `/health` extension-id gate.

## [1.58.3.0] - 2026-06-18

## **GBrowser masks the full set of automation tells by default, on every path a page can reach.**
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.58.3.0
1.58.4.0
17 changes: 16 additions & 1 deletion browse/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,12 @@ function sanitizeAuthToken(raw: string | undefined): string | null {
// env once via resolveConfigFromEnv() and threads the result through.
const BROWSE_PORT = parseInt(process.env.BROWSE_PORT || '0', 10);
const IDLE_TIMEOUT_MS = parseInt(process.env.BROWSE_IDLE_TIMEOUT || '1800000', 10); // 30 min
// Optional pin: when set, /health only hands the bootstrap token to OUR
// extension origin (chrome-extension://<id>), not any extension on the
// machine. Mirrors the same env var the PTY agent uses for its /ws origin
// gate (terminal-agent.ts). Unset = backward-compatible (any extension
// origin), which is why this is opt-in tightening, not a behavior break.
const BROWSE_EXTENSION_ID = process.env.BROWSE_EXTENSION_ID || '';

/**
* Port the local listener bound to. Set once the daemon picks a port.
Expand Down Expand Up @@ -1782,8 +1788,17 @@ export function buildFetchHandler(cfg: ServerConfig): ServerHandle {
// server is tunneled to the internet (ngrok, SSH tunnel).
// In headed mode the server is always local, so return token unconditionally
// (fixes Playwright Chromium extensions that don't send Origin header).
//
// Extension-origin path: when BROWSE_EXTENSION_ID is pinned, require
// an EXACT chrome-extension://<id> match so a SECOND extension on the
// same machine can't scrape the root token by hitting /health (the
// multi-extension privilege-escalation leak; mirrors the /ws origin
// gate in terminal-agent.ts). Unset = any extension origin, preserving
// today's behavior for installs that don't pin the id.
...(browserManager.getConnectionMode() === 'headed' ||
req.headers.get('origin')?.startsWith('chrome-extension://')
(req.headers.get('origin')?.startsWith('chrome-extension://') &&
(!BROWSE_EXTENSION_ID ||
req.headers.get('origin') === `chrome-extension://${BROWSE_EXTENSION_ID}`))
? { token: authToken } : {}),
// The chat queue is gone — Terminal pane is the sole sidebar
// surface. Keep `chatEnabled: false` so any older extension
Expand Down
10 changes: 10 additions & 0 deletions browse/test/server-auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,16 @@ describe('Server auth security', () => {
expect(healthBlock).toContain('chrome-extension://');
});

// Test 1a: /health extension-origin path honors the BROWSE_EXTENSION_ID pin
// so a SECOND extension on the machine can't scrape the root token. Mirrors
// the /ws origin gate in terminal-agent.ts (multi-extension priv-esc leak).
test('/health extension token gate honors BROWSE_EXTENSION_ID exact match', () => {
const healthBlock = sliceBetween(SERVER_SRC, "url.pathname === '/health'", "url.pathname === '/connect'");
// The gate must consult the pin, not just any chrome-extension:// origin.
expect(healthBlock).toContain('BROWSE_EXTENSION_ID');
expect(healthBlock).toContain('chrome-extension://${BROWSE_EXTENSION_ID}');
});

// Test 1b: /health does not expose sensitive browsing state
test('/health does not expose currentUrl or currentMessage', () => {
const healthBlock = sliceBetween(SERVER_SRC, "url.pathname === '/health'", "url.pathname === '/connect'");
Expand Down
28 changes: 14 additions & 14 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 8 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,14 @@
"slop:diff": "bun run scripts/slop-diff.ts"
},
"dependencies": {
"@huggingface/transformers": "^4.1.0",
"@huggingface/transformers": "^4.2.0",
"@ngrok/ngrok": "^1.7.0",
"diff": "^7.0.0",
"diff": "^8.0.4",
"html-to-docx": "1.8.0",
"marked": "^18.0.2",
"playwright": "^1.58.2",
"puppeteer-core": "^24.40.0",
"socks": "^2.8.8"
"marked": "^18.0.5",
"playwright": "^1.61.0",
"puppeteer-core": "^24.43.1",
"socks": "^2.8.9"
},
"engines": {
"bun": ">=1.0.0"
Expand All @@ -73,8 +73,8 @@
],
"devDependencies": {
"@anthropic-ai/claude-agent-sdk": "0.2.117",
"@anthropic-ai/sdk": "^0.78.0",
"xterm": "5",
"@anthropic-ai/sdk": "^0.91.1",
"xterm": "^5.3.0",
"xterm-addon-fit": "^0.8.0"
}
}
Loading