Affected versions: confirmed on tag ext-v0.3.0 (commit 75e2c64, 2026-09-16), the latest tag available at time of report, matching HEAD exactly.
Summary
BrowserSkill's local daemon (bsk daemon start) runs a WebSocket server (default port 52800) that a companion browser extension connects to, giving the daemon (and, through it, any AI agent driving it) full control of the user's real, already-logged-in browser: reading pages, taking screenshots, filling forms, and interacting with any site the user is signed into. The daemon's only access control for this local WebSocket server was origin_allowed() (crates/bsk-cli/src/daemon/ws.rs), which checks that the connecting page's Origin header is shaped like a Chrome extension origin (chrome-extension:// followed by exactly 32 lowercase a-p characters, the MV3 extension-id format), but never checks which extension it actually is. Every browser extension has an origin of exactly this shape, so any other extension installed in the same browser profile (a malicious one, a compromised or supply-chain-attacked legitimate extension, or one the user was tricked into installing) can open a WebSocket connection to this well-known local port and be treated identically to BrowserSkill's own extension, gaining full control of the user's browser and its logged-in sessions.
Details
crates/bsk-cli/src/daemon/ws.rs (before a fix), with the code's own doc comment preserved:
/// Result of the optional Origin allow-list check.
///
/// TODO(M10/M12): pair v0.1 GA with an actual extension-id allow-list
/// (`DaemonConfig::allowed_extension_ids: HashSet<String>`) populated
/// from a config file / pairing flow, rather than accepting any
/// extension-shaped origin. Review M4/M5 I8, acceptable defense-in-
/// depth gap for now because pairing happens through the popup, but
/// a side-loaded extension on the same machine currently passes the
/// gate.
pub(super) fn origin_allowed(origin: &str, allow_any: bool) -> bool {
if allow_any {
return true;
}
let prefix = "chrome-extension://";
if let Some(rest) = origin.strip_prefix(prefix) {
if rest.len() == 32 && rest.bytes().all(|b| (b'a'..=b'p').contains(&b)) {
return true;
}
}
false
}
handle_connection calls this once, during the WS opening handshake, and nothing afterward re-checks the caller's identity before dispatching full tool.* RPCs (browser automation commands) over the connection. The daemon's default local port (52800) is a published constant (DEFAULT_WS_PORT), and no additional secret is required to reach it, since the only local-mode gate is this shape check.
POC
(available upon request)
Impact
Any other browser extension installed in the same browser profile as BrowserSkill's own extension can drive the daemon and, through it, the user's real, already-logged-in browser: navigating pages, reading page content and screenshots, and submitting forms on any site the user is authenticated to (email, banking, internal tools, source control, etc.), with no interaction from the user and no indication that a different extension is in control. This converts "install any other browser extension, ever" into a path to full browser-session takeover on a machine running BrowserSkill's daemon.
Affected versions: confirmed on tag
ext-v0.3.0(commit75e2c64, 2026-09-16), the latest tag available at time of report, matching HEAD exactly.Summary
BrowserSkill's local daemon (
bsk daemon start) runs a WebSocket server (default port 52800) that a companion browser extension connects to, giving the daemon (and, through it, any AI agent driving it) full control of the user's real, already-logged-in browser: reading pages, taking screenshots, filling forms, and interacting with any site the user is signed into. The daemon's only access control for this local WebSocket server wasorigin_allowed()(crates/bsk-cli/src/daemon/ws.rs), which checks that the connecting page'sOriginheader is shaped like a Chrome extension origin (chrome-extension://followed by exactly 32 lowercasea-pcharacters, the MV3 extension-id format), but never checks which extension it actually is. Every browser extension has an origin of exactly this shape, so any other extension installed in the same browser profile (a malicious one, a compromised or supply-chain-attacked legitimate extension, or one the user was tricked into installing) can open a WebSocket connection to this well-known local port and be treated identically to BrowserSkill's own extension, gaining full control of the user's browser and its logged-in sessions.Details
crates/bsk-cli/src/daemon/ws.rs(before a fix), with the code's own doc comment preserved:handle_connectioncalls this once, during the WS opening handshake, and nothing afterward re-checks the caller's identity before dispatching fulltool.*RPCs (browser automation commands) over the connection. The daemon's default local port (52800) is a published constant (DEFAULT_WS_PORT), and no additional secret is required to reach it, since the only local-mode gate is this shape check.POC
(available upon request)
Impact
Any other browser extension installed in the same browser profile as BrowserSkill's own extension can drive the daemon and, through it, the user's real, already-logged-in browser: navigating pages, reading page content and screenshots, and submitting forms on any site the user is authenticated to (email, banking, internal tools, source control, etc.), with no interaction from the user and no indication that a different extension is in control. This converts "install any other browser extension, ever" into a path to full browser-session takeover on a machine running BrowserSkill's daemon.