aboutsummaryrefslogtreecommitdiff
path: root/SECURITY.md
diff options
context:
space:
mode:
authorTianren Dong <tianrendong.dtr@gmail.com>2026-05-16 13:13:32 -0400
committerTianren Dong <tianrendong.dtr@gmail.com>2026-05-16 13:13:48 -0400
commitb2ca8f8c343281c0263bda4c043cbe7f0d7d442e (patch)
treec37f274c112a202af86144fd6034a8a54d7933eb /SECURITY.md
parentea567819d835d4704d9fb38cd114e0e5b231759e (diff)
Revert 0.16.x and 0.17.x: restore tree to v0.15.20 state
Reverts the pairing + signed-envelopes work (0.16.0-0.16.1), the standalone daemon refactor (0.17.0-0.17.3), and assorted patches. These were unpublished from npm; this commit restores the working tree to match the v0.15.20 release so the branch tip and the npm 'latest' dist-tag are consistent again. Tags v0.16.0 through v0.17.3 are preserved as recovery anchors; the work is reachable via 'git checkout v0.17.3' if ever needed. Tree-equivalent to v0.15.20.
Diffstat (limited to 'SECURITY.md')
-rw-r--r--SECURITY.md38
1 files changed, 8 insertions, 30 deletions
diff --git a/SECURITY.md b/SECURITY.md
index 943352b..ffc2c06 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -6,39 +6,17 @@ Open a GitHub issue prefixed with `[security]` at https://github.com/tianrendong
## Threat model
-`pi-chrome` is a developer tool you install knowingly.
+`pi-chrome` is a developer tool you install knowingly. It is **not** designed to defend against:
-### In scope (designed to defend against)
+- Hostile pages running in your Chrome trying to detect or escape automation. (Standard browser security boundaries still apply, but a hostile page that already runs in your tab can do anything that page can already do.)
+- Other processes on your local machine. The bridge binds to `127.0.0.1:17318` (loopback only) and chrome_* tools require `/chrome authorize` inside Pi, but the bridge does not authenticate arbitrary non-browser local callers. If your threat model includes hostile local processes running as you, run pi-chrome on a separate user account.
-- **Ordinary web pages.** The bridge rejects browser-origin command requests; CORS cannot be used to drive Chrome.
-- **Drive-by local processes / curl without the pairing secret.** Every privileged endpoint (`/next`, `/result`, `/command`) requires a signed envelope with an HMAC key established during explicit pairing. A random local process that has not been paired cannot poll commands, post results, or invoke `/command`.
-- **Malicious Chrome extensions on the same browser.** The bridge pins the exact `chrome-extension://<id>` origin established at pairing time. Any other extension polling `/next` receives only an unsigned idle response (`type:"none"` + `needsPairing:true`); it never sees queued commands. An attacker who somehow learns a command ID still cannot forge a `/result` without the extension's HMAC key. The unsigned idle path exists purely so older or unrelated extensions can read the `x-pi-chrome-version` header and trigger the auto-reload migration path.
-- **Pre-bind localhost impostors.** Pairing establishes mutual HMAC keys before any command is delivered. The Pi peer that talks to the bridge owner via `/command` signs every request and verifies the owner's signed response, so a process that binds `:17318` first cannot impersonate either side without the keys.
-- **Replay / interleaving.** Every signed envelope binds protocol version, direction, method, path, extension ID, bridge ID, timestamp, fresh nonce, and SHA-256 of the body. Timestamps must be within ±30 s and nonces are rejected on reuse.
-- **`/chrome authorize` bypass.** Even an attacker who can reach the bridge cannot drive Chrome from chrome_* tools unless the current Pi session also authorized control.
+`pi-chrome` **is** designed to:
-### Out of scope (cannot defend against)
-
-- **Same-user malware.** A process running under your user account can read `~/.config/pi/chrome-bridge.json` (mode 0600), replace the Chrome extension, attach a debugger to Pi or Chrome, or use OS automation APIs. If your threat model includes hostile local processes running as you, isolate pi-chrome on a separate user account or VM.
-- **Hostile pages running in your tabs.** Standard browser security boundaries apply. A page that already runs in a tab can read its own DOM. MAIN-world helpers (`window.__PI_CHROME_STATE__`) are page-tamperable; treat snapshot UIDs as advisory for dangerous actions on untrusted origins.
-- **Network adversaries.** All communication is loopback. There is no network path to defend.
-
-### Properties (always-on)
-
-- **Loopback only.** Every endpoint rejects non-loopback `remoteAddress`. `PI_CHROME_BRIDGE_HOST=0.0.0.0` is ignored unless `PI_CHROME_BRIDGE_DANGEROUS_REMOTE=1` is set, and even then the per-endpoint loopback check still rejects remote callers.
-- **Resource caps.** 1 MiB request body cap (413), 256 queued/pending commands, 4 concurrent long-pollers.
-- **No telemetry.** All state stays on disk under your config dir or in the extension's `chrome.storage.local`.
-- **Fail-closed.** Missing or invalid auth headers, expired timestamps, replayed nonces, and origin mismatches all return errors with no side effects.
-
-## Pairing flow
-
-1. Run `/chrome pair` in Pi. Pi generates a 32-byte invite, prints `pcp_<base64url>` (also copied to clipboard on macOS), and arms `/pair` for 10 minutes.
-2. Open the **Pi Chrome Connector** extension popup in Chrome, paste the invite, click *Pair*.
-3. The extension signs its identity with the invite key. The bridge verifies, derives two HKDF keys (one for extension↔bridge `/next`/`/result`, one for peer Pi↔owner Pi `/command`), pins the exact extension ID, and returns *only* the `extensionPairKey` over a response signed with the invite secret (so the extension proves it's talking to the same Pi process that issued the invite). The `brokerKey` stays Pi-side, persisted in `~/.config/pi/chrome-bridge.json`, and is never sent to the browser.
-4. Both sides persist their copies: Pi at `~/.config/pi/chrome-bridge.json` mode 0600; extension in `chrome.storage.local`.
-5. The invite is destroyed.
-
-Run `/chrome unpair` to clear pairing keys; every Pi session and the Chrome extension popup will then need to re-pair.
+- Never exfiltrate page state to the network. All communication is loopback (`127.0.0.1`).
+- Surface every action with an honest result envelope so the agent can't silently do the wrong thing.
+- Keep Chrome control locked until the user explicitly runs `/chrome authorize` in the current Pi session.
+- Reject browser-origin command requests to the loopback bridge so ordinary web pages cannot use CORS to drive Chrome.
## The companion extension