Skip to content

fix(relay): refuse profile-wide CDP clears unless the user allows them - #27

Open
ivalsaraj wants to merge 1 commit into
mainfrom
fix/block-profile-wide-cdp-clears
Open

ivalsaraj wants to merge 1 commit into
mainfrom
fix/block-profile-wide-cdp-clears

Conversation

@ivalsaraj

Copy link
Copy Markdown
Owner

The hole

Network.clearBrowserCookies is not scoped to the calling tab. Through chrome.debugger it clears every cookie for every domain in the user's Chrome profile — mail, source control, banking, all at once.

The relay forwarded it. It is not in INIT_ONLY_METHODS and was intercepted nowhere, so it fell through _forwardToTab_sendToExt('cdpCommand')chrome.debugger.sendCommand. Same for Network.clearBrowserCache and Storage.clearCookies. Chrome offers no scoped variant of any of them.

$ rg -n "clearBrowserCookies|clearBrowserCache|BLOCKED_CDP" relay/src mcp/src extension/
(no matches)

The exposure is structural to this product, not incidental: a tool driving a throwaway Chromium loses a temp profile, while BrowserForce advertises the user's real profile to every connected CDP client by design. Our core value proposition is the blast radius.

Not hypothetical — a competitor shipped this, hit it, and wrote it up: "It wiped Gmail, GitHub, and all authenticated sessions."

The fix

A PROFILE_WIDE_CLEAR_METHODS guard in _handleCdpClientMessage, before routing — these arrive on both paths: tab-scoped with a sessionId (Network.*) and browser-scoped without one (Storage.clearCookies).

Permission is read through _getRestrictionsSafe(), so an unreadable setting refuses rather than defaulting to granted.

The opt-in is a popup checkbox, not an env var. An agent runs shell commands and could export an env var for itself; it cannot tick a checkbox. Default off.

Scoped operations stay allowed, and the refusal names them so an agent recovers without interrupting the user:

Network.clearBrowserCookies is blocked by BrowserForce: it clears data for EVERY site in the user's Chrome profile, not just this tab, and would sign them out everywhere. To clear one site, read its cookies with Network.getCookies({ urls: [...] }) and remove them individually with Network.deleteCookies, or scope storage with Storage.clearDataForOrigin. If the user truly wants the entire profile cleared, ask them first, then have them tick "Allow profile-wide data clearing" in the BrowserForce extension popup.

Tests — and why the first draft was wrong

6 new tests. The first version sent these commands without a sessionId, which routes to the browser-level handler that answers {} for anything it doesn't know. Those tests would have passed with or without the guard. Rewritten to drive the real Playwright path: auto-attach, then issue against the page session id.

Verified red by removing the guard: the three refusals and the fail-closed case fail; the two must-still-work cases stay green — correct polarity in both directions.

  • pnpm test995 tests, 0 failures, exit 0 (was 989)

Docs

[DOCS UPDATED: AGENTS.md (Security Rules), skills/browserforce/SKILL.md (Rules #6)]

🤖 Generated with Claude Code

https://claude.ai/code/session_019jMKr1zqWLLffD5h1vhwMH

Network.clearBrowserCookies is not scoped to the calling tab. Through
chrome.debugger it clears every cookie for every domain in the user's Chrome
profile, signing them out of mail, source control and banking at once. The
relay forwarded it: it is not in INIT_ONLY_METHODS and was intercepted
nowhere, so it fell through _forwardToTab to the extension. Same for
Network.clearBrowserCache and Storage.clearCookies. Chrome offers no scoped
variant of any of them.

The exposure is structural to this product rather than incidental. Tools that
drive a throwaway Chromium lose a temp profile; BrowserForce advertises the
user's real profile to every connected CDP client by design, so the blast
radius is the whole browser.

The guard sits in _handleCdpClientMessage before routing, because these
commands arrive on both paths: tab-scoped with a sessionId (Network.*) and
browser-scoped without one (Storage.clearCookies). It reads the permission
through _getRestrictionsSafe(), so an unreadable setting refuses rather than
defaulting to granted.

The opt-in is a popup checkbox, not an env var. An agent runs shell commands
and could export an env var for itself; it cannot tick a checkbox. Scoped
operations stay allowed and the refusal names them, so an agent recovers
without asking the user at all: Network.getCookies + Network.deleteCookies for
one site, Storage.clearDataForOrigin for one origin.

Tests drive the real Playwright path — auto-attach, then issue the command
against the page session id. Sent without a sessionId these reach the
browser-level handler, which answers {} for anything it does not know, so such
a test would pass with or without the guard. Verified red by removing the
guard: the three refusals and the fail-closed case fail, while the two
must-still-work cases stay green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019jMKr1zqWLLffD5h1vhwMH
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