Skip to content

Threat model

This page describes the security shape of bproxy as it ships today — the trust boundaries between processes and files, the credentials that cross each boundary, and the STRIDE-class threats each boundary mitigates. The view documents current code and configuration; speculative changes and out-of-scope hardening are listed near the bottom.

---
title: bproxy — Threat model
---
flowchart TB
  subgraph fs ["File-system boundary (user, mode 0600)"]
    Token["~/.bproxy/token
daemon bearer"] ExtToken["~/.bproxy/extension-token
WS auth (restart-transparent)"] PidLock["~/.bproxy/bproxy.pid
single-daemon lock"] Logs["~/.bproxy/logs/YYYY-MM-DD.log"] end subgraph host ["Localhost boundary (127.0.0.1 only)"] CLI["CLI process"] Daemon["Daemon (Fastify)
127.0.0.1:9615"] end subgraph extOrigin ["Extension origin (chrome-extension://)"] BG["Background SW"] Popup["Pairing popup"] CS["Runtime content script
ISOLATED world"] Trace["chrome.storage.session
trace / dedupe / injected tabs"] Bootstrap["chrome.storage.local
bootstrap + config flags"] end Page["Web page"] CLI -- "(1) POST /
Authorization: Bearer {daemon-token}" --> Daemon Popup -- "(2) POST /pair/claim
one-time code · Origin: chrome-extension://" --> Daemon BG <-- "(3) GET /ws
Sec-WebSocket-Protocol: bproxy.v1, auth.{extension-token}" --> Daemon BG -- "(4) chrome.scripting.executeScript" --> CS CS -- "(5) DOM read/write" --> Page CLI -. "0600 + owner check" .-> Token Daemon -. writes .-> Token Daemon -. writes .-> ExtToken Daemon -. writes .-> PidLock Daemon -. writes .-> Logs Popup -. writes .-> Bootstrap BG -. reads .-> Bootstrap BG -. reads/writes .-> Trace classDef boundary stroke:#dc2626,stroke-width:2px,stroke-dasharray:4 2; class fs,host,extOrigin boundary;

Figure 5. Data-flow diagram with trust boundaries — the three dashed-red enclosures (filesystem, localhost, extension origin) bound material that the daemon and extension protect; numbered edges (1)–(5) anchor the STRIDE entries below.

ClassThreatMitigationAnchor
SpoofingAnother process binds the daemon port for the same userPID lockfile per BPROXY_HOME; start exits non-zero when the lock points at a live PIDlifecycle.ts:startDetached; Gap E “start fails cleanly when daemon already running”
SpoofingOther-user process reads the daemon tokenToken file mode 0600 + owner UID check; daemon refuses to start (and CLI must refuse to use) any token with wrong mode/ownerlifecycle.ts:assertOwnerMode600 (INSECURE_TOKEN_FILE); Gap E “token file is created and readable only by owner”
SpoofingCross-site fetch from a malicious page reaches the daemonThree-layer header gate at onRequest: Host pinned to 127.0.0.1:port / localhost:port; Origin must be absent (CLI) or chrome-extension://* (popup/WS); Sec-Fetch-Site rejected unless none / same-originauth.ts:checkHost/checkOrigin/checkFetchSite; Gap C negative tests
SpoofingWrong extension instance claims the pairing codeOne-time consumption + 5-min TTL + constant-time compare + chrome-extension:// Origin required; single-active-token policy invalidates previously claimed extension tokenspairing.ts; auth.ts:checkOrigin('pair'); service spec § Pairing bootstrap route
TamperingDaemon token file is replaced by another userOwner check rejects tokens whose st.uid differs from process.getuid()lifecycle.ts:assertOwnerMode600
Repudiation”Did command X run? When? Through which client?”Every lifecycle event carries request id: receivedpacing_wait?forwardedresponse (or timeout / replay)daemon observability suites
Information disclosureDaemon API exposed beyond localhostBind host fixed to 127.0.0.1; Host header verified at the auth gate even if a proxy rewrites itconfig.ts; auth.ts:checkHost
Information disclosureToken leaks via insecure file modeRead-side preflight on every token load fails closed with INSECURE_TOKEN_FILE / INSECURE_EXTENSION_TOKEN_FILElifecycle.ts:assertOwnerMode600; Gap E file-semantics tests
Denial of serviceUnbounded pending-request mapHard cap of 100 in-flight requests → OVERLOADEDpending.ts; pending.test.ts
Denial of serviceRepeated read commands grow daemon memory without boundElement-handle cache is bounded (TTL 120s, per-scope cap 200, global cap 1000)element-handles.ts; handle-cache tests
Denial of serviceHead-of-line blocking across tabsPer-tab FIFO queue, parallel across tabsdispatch.ts:withTabLock; dispatch.test.ts
Denial of servicePairing-code brute forceOne-time consumption + 5-min TTL + constant-time compare + chrome-extension:// Origin gate + global failed-attempt throttle on /pair/claim (5 failures / 60s). The throttle is localhost-scoped best-effort, not per-source attribution or DDoS-grade protection.pairing.ts; pairing-rate-limit.ts
Elevation of privilegeExtension token grants command issuanceTwo-token model: bearer auth only valid on POST /; subprotocol auth only valid on GET /ws; tokens never cross routesauth.ts:checkCommandAuth/checkWsAuth
Elevation of privilegePairing endpoint accepts CLI bearerPOST /pair/claim is body-auth only (pairing code) and requires chrome-extension:// Originservice spec § Auth Gate
SurfaceRiskShipped mitigation
Bootstrap token in extension storageLong-lived WS auth material could leak through loose storage handlingPopup validates payload shape before write; bootstrap is stored as one atomic chrome.storage.local record; daemon still limits auth to localhost WS with subprotocol token
Runtime content scriptPage-visible extension presence or broad ambient listenersNo declarative content_scripts; runtime script is injected only on first command per tab; content host keeps one chrome.runtime.onMessage listener and no page-global hooks
MAIN-world executionPage learns about the extension or receives raw extension errors/stacksMAIN world is one-shot only for narrow product actions such as runtime-api writes via chrome.scripting.executeScript({ world: "MAIN" }); injected functions catch/normalize errors and contain no identifying literals (ADR-013, ADR-015, ADR-024)
Trace / dedupe ring bufferdebug.log could expose stale or over-broad extension dataTrace is bounded in chrome.storage.session, queryable only through authenticated daemon forwarding, filtered by id/limit, and stamped with extensionVersion
Polling / DOM settleHigh-signal instrumentation or bundle hygiene regressionsNo MutationObserver; jittered polling only; Task 16 scans the production artifact to keep MutationObserver out of shipped output
Screenshot escalationchrome.debugger would widen capability and show a user-visible Chrome bannerNormal screenshots use captureVisibleTab; debugger screenshots remain gated behind DEBUGGER_DISABLED, with no debugger permission in the manifest today
Web-accessible resourcesDeterministic extension-resource probing by pages or scannersweb_accessible_resources is absent by default; build hook strips WXT’s empty array stub so the manifest stays default-deny (ADR-016)
Hidden-tab destructive actionsClicking, hovering, or writing to a background tab may produce misleading state or bot-signal issuesContent polling checks document.visibilityState and destructive actions bail with TAB_NOT_VISIBLE unless future protocol metadata explicitly opts into user-initiated hidden-tab behavior
Navigation push over WSTop-level navigation events carry Chrome tab idsTab ids stay inside the daemon↔extension boundary only; they are used for page-epoch tracking and are never exposed to CLI stdout, agent-visible responses, or public handle strings
  • Process-level sandboxing of the daemon (e.g. seccomp, App Sandbox profile)
  • Cross-host operation or TLS; bproxy remains localhost-only
  • Closed shadow-root support
  • A shipped opt-in path for chrome.debugger screenshots
  • Arbitrary page eval; page/runtime investigation belongs to CDP/devtools outside bproxy
  • Containers — the runtime processes and wire protocols whose boundaries this view analyses.
  • Deployment — where each process and state file lives on the operator’s machine.
  • Session state — daemon pause/bind gating that sits behind the transport.

For normative implementation details, see Proxy Daemon and Browser Extension.