@mehmoodqureshi/chrome-mcp 0.4.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/config.js +5 -0
- package/dist/src/mcp/tools.js +12 -9
- package/dist/src/security/policy.d.ts +10 -3
- package/dist/src/security/policy.js +10 -3
- package/docs/BLUEPRINT.md +2 -2
- package/extension-dist/manifest.json +20 -5
- package/package.json +1 -1
package/dist/src/config.js
CHANGED
|
@@ -123,6 +123,11 @@ function parseArgs(argv) {
|
|
|
123
123
|
}
|
|
124
124
|
// File first, then flags win.
|
|
125
125
|
const policy = (0, policy_1.resolvePolicy)({ ...policyFile, ...policyFlags });
|
|
126
|
+
// Uploads must be confined to a directory — refuse to start with uploads enabled
|
|
127
|
+
// but no dir, rather than silently allow unrestricted local-file access.
|
|
128
|
+
if (policy.allowUploads && !policy.uploadsDir) {
|
|
129
|
+
throw new Error('--enable-uploads requires --uploads-dir <path> (uploads must be confined to a directory)');
|
|
130
|
+
}
|
|
126
131
|
return {
|
|
127
132
|
wsPort,
|
|
128
133
|
dataDir: resolveDataDir(),
|
package/dist/src/mcp/tools.js
CHANGED
|
@@ -268,15 +268,18 @@ exports.TOOL_HANDLERS = {
|
|
|
268
268
|
const files = (0, validators_1.optionalStringArray)(a, 'files');
|
|
269
269
|
if (!files || files.length === 0)
|
|
270
270
|
throw new validators_1.McpToolError('"files" must be a non-empty array of absolute local paths');
|
|
271
|
-
// Path restriction:
|
|
272
|
-
//
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
271
|
+
// Path restriction: uploads MUST be confined to a configured directory. Without
|
|
272
|
+
// one, any absolute path (e.g. ~/.ssh/id_rsa) could be uploaded to a page, so we
|
|
273
|
+
// refuse rather than allow unrestricted local-file access. With a dir, every file
|
|
274
|
+
// must resolve inside it (blocks `..` traversal and arbitrary-file exfiltration).
|
|
275
|
+
if (!ctx.policy.uploadsDir) {
|
|
276
|
+
throw new validators_1.McpToolError('upload denied: uploads require an --uploads-dir to be configured (refusing unrestricted local-file access)');
|
|
277
|
+
}
|
|
278
|
+
const dir = (0, node_path_1.resolve)(ctx.policy.uploadsDir);
|
|
279
|
+
for (const f of files) {
|
|
280
|
+
const abs = (0, node_path_1.resolve)(f);
|
|
281
|
+
if (abs !== dir && !abs.startsWith(dir + node_path_1.sep)) {
|
|
282
|
+
throw new validators_1.McpToolError(`upload denied: "${f}" is outside the allowed uploads dir (${dir})`);
|
|
280
283
|
}
|
|
281
284
|
}
|
|
282
285
|
return (0, envelopes_1.jsonResult)(await ctx.ex.uploadFile(t, files, { tabId: tabId(a) }));
|
|
@@ -6,9 +6,16 @@
|
|
|
6
6
|
* threat, so the policy:
|
|
7
7
|
* - is ON by default with a SAFE default (empty allowlist, no eval, no
|
|
8
8
|
* downloads, mutations disabled),
|
|
9
|
-
* - gates READS as well as writes (reads are the exfil payload)
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* - gates READS as well as writes (reads are the exfil payload).
|
|
10
|
+
*
|
|
11
|
+
* SCOPE OF ENFORCEMENT: this policy is enforced SERVER-SIDE ONLY, at the executor
|
|
12
|
+
* dispatch chokepoint (`assertUrlAllowed` in src/mcp/tools.ts). The extension does
|
|
13
|
+
* NOT independently re-check the policy, so the bridge token is the sole trust
|
|
14
|
+
* boundary for the WebSocket: any client holding the token can issue commands the
|
|
15
|
+
* extension will execute, regardless of this policy. The policy constrains the
|
|
16
|
+
* official MCP server (and thus the LLM driving it); it is not a second line of
|
|
17
|
+
* defense against a rogue token-holding client. (A future hardening would mirror
|
|
18
|
+
* this gate inside the extension router via a policy delivered in the welcome frame.)
|
|
12
19
|
*
|
|
13
20
|
* `assertUrlAllowed(url, method, policy)` is the single chokepoint. It throws an
|
|
14
21
|
* `ExecutorError('POLICY_DENIED', …)` which the never-throw dispatch firewall
|
|
@@ -7,9 +7,16 @@
|
|
|
7
7
|
* threat, so the policy:
|
|
8
8
|
* - is ON by default with a SAFE default (empty allowlist, no eval, no
|
|
9
9
|
* downloads, mutations disabled),
|
|
10
|
-
* - gates READS as well as writes (reads are the exfil payload)
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* - gates READS as well as writes (reads are the exfil payload).
|
|
11
|
+
*
|
|
12
|
+
* SCOPE OF ENFORCEMENT: this policy is enforced SERVER-SIDE ONLY, at the executor
|
|
13
|
+
* dispatch chokepoint (`assertUrlAllowed` in src/mcp/tools.ts). The extension does
|
|
14
|
+
* NOT independently re-check the policy, so the bridge token is the sole trust
|
|
15
|
+
* boundary for the WebSocket: any client holding the token can issue commands the
|
|
16
|
+
* extension will execute, regardless of this policy. The policy constrains the
|
|
17
|
+
* official MCP server (and thus the LLM driving it); it is not a second line of
|
|
18
|
+
* defense against a rogue token-holding client. (A future hardening would mirror
|
|
19
|
+
* this gate inside the extension router via a policy delivered in the welcome frame.)
|
|
13
20
|
*
|
|
14
21
|
* `assertUrlAllowed(url, method, policy)` is the single chokepoint. It throws an
|
|
15
22
|
* `ExecutorError('POLICY_DENIED', …)` which the never-throw dispatch firewall
|
package/docs/BLUEPRINT.md
CHANGED
|
@@ -64,7 +64,7 @@ takes over from CDP and vice-versa on disconnect.
|
|
|
64
64
|
- **The 256-bit per-boot token is the ONLY security boundary.** Origin checks and the loopback bind are defense-in-depth against *browser-page* attackers, not native processes. [RESOLVED — security blockers 2 & 3]
|
|
65
65
|
- **One canonical `protocol.ts`** imported by both server and extension. Four conflicting wire/port/token designs are collapsed into this single contract. [RESOLVED — integration blocker 1]
|
|
66
66
|
- **Helpers (`extract_links`, `read_as_markdown`, `fill_form`) are composed server-side** from primitives. Only `download_file` is an executor/wire method. [RESOLVED — integration blocker 2]
|
|
67
|
-
- **Default-deny domain policy is ON by default and gates reads too**, enforced inside the executor dispatch (both backends)
|
|
67
|
+
- **Default-deny domain policy is ON by default and gates reads too**, enforced SERVER-SIDE inside the executor dispatch (both backends). NOTE: the extension router does NOT independently re-check the policy — the bridge token is the sole trust boundary for the WebSocket. Mirroring the gate into the extension router is a planned hardening. [RESOLVED — security major 4 & eval]
|
|
68
68
|
|
|
69
69
|
---
|
|
70
70
|
|
|
@@ -540,7 +540,7 @@ threat** (this tool feeds untrusted page text to an LLM that can call `eval`).
|
|
|
540
540
|
3. **Origin is NOT a security layer.** Documented as defense-in-depth against *browser-page* attackers only (a web page cannot forge a `chrome-extension://` Origin; a native process can). The token holds independently. We never relax token strength on Origin's account. Never reject a valid-token dev connection solely on Origin mismatch. [RESOLVED — blocker 2, mv3 major Origin]
|
|
541
541
|
4. **No `/connect` HTTP endpoint.** Token bootstrap is the **native-messaging trampoline** reading the 0600 file (the model/attacker can't read it), with a manual file-path-pairing fallback. No race-able network token vendor exists. [RESOLVED — blocker 3]
|
|
542
542
|
5. **Token never logged.** Never on stdout or stderr; only in the 0600 file (mode verified post-write, fail-closed). Any human-readable pairing artifact is also 0600 and short-lived. `policy.test.ts` asserts the token appears on neither stream. `logErr` redacts. [RESOLVED — major 5]
|
|
543
|
-
6. **Default-deny domain policy, ON by default, gating READS too.** `Policy { allowDomains: glob[]; allowEval; allowDownloads; allowAllTabs }`. Absent config → **SAFE DEFAULT**: navigate only to `about:blank` + already-open allowlisted tabs, **eval denied cross-domain, reads (`get_text`/`get_html`/`screenshot`/`eval`) denied outside the allowlist**, downloads off. `assertUrlAllowed(currentTabUrl, method)` runs in **the executor dispatch (both backends)
|
|
543
|
+
6. **Default-deny domain policy, ON by default, gating READS too.** `Policy { allowDomains: glob[]; allowEval; allowDownloads; allowAllTabs }`. Absent config → **SAFE DEFAULT**: navigate only to `about:blank` + already-open allowlisted tabs, **eval denied cross-domain, reads (`get_text`/`get_html`/`screenshot`/`eval`) denied outside the allowlist**, downloads off. `assertUrlAllowed(currentTabUrl, method)` runs SERVER-SIDE in **the executor dispatch (both backends)** before any attach/navigate/eval/read. The extension router does NOT re-check the policy (token is the sole bridge boundary); a mirrored extension-side gate is a planned hardening. `--unsafe-all-domains` (= `allowDomains:['*']`) is the loud-logged escape hatch. [RESOLVED — major 4]
|
|
544
544
|
7. **Safe-mode shipped in v1 (not "future").** Default disables `eval` and the entire mutating tool set; `--enable-mutations` / `--unsafe-enable-eval` opt in. `eval`'s effective target origin (the tab's current URL) is allowlist-checked before dispatch. [RESOLVED — major eval]
|
|
545
545
|
8. **Narrowed manifest:** no `<all_urls>`; `host_permissions:[]` + `optional_host_permissions` requested on demand after an explicit user action-click grant before first attach. [RESOLVED — major 4]
|
|
546
546
|
9. **Displacement is a security event:** superseding connects are logged loudly + surfaced in `chrome_status`; the active connection is pinned to the first `hello` ext id and won't be displaced by a different id without re-pair. [RESOLVED — minor 9]
|
|
@@ -1,12 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest_version": 3,
|
|
3
3
|
"name": "Chrome MCP Bridge",
|
|
4
|
-
"version": "0.4.
|
|
4
|
+
"version": "0.4.1",
|
|
5
5
|
"description": "Lets a local chrome-mcp server drive this browser. Pair it with the server's handshake token.",
|
|
6
6
|
"minimum_chrome_version": "116",
|
|
7
|
-
"background": {
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
"background": {
|
|
8
|
+
"service_worker": "background.js"
|
|
9
|
+
},
|
|
10
|
+
"permissions": [
|
|
11
|
+
"tabs",
|
|
12
|
+
"scripting",
|
|
13
|
+
"activeTab",
|
|
14
|
+
"downloads",
|
|
15
|
+
"storage",
|
|
16
|
+
"alarms",
|
|
17
|
+
"cookies",
|
|
18
|
+
"debugger"
|
|
19
|
+
],
|
|
20
|
+
"host_permissions": [
|
|
21
|
+
"<all_urls>"
|
|
22
|
+
],
|
|
10
23
|
"options_page": "options.html",
|
|
11
|
-
"action": {
|
|
24
|
+
"action": {
|
|
25
|
+
"default_title": "Chrome MCP — open options to pair"
|
|
26
|
+
}
|
|
12
27
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mehmoodqureshi/chrome-mcp",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Drive a real Chrome browser over MCP. A stdio MCP server (CLI) plus an MV3 extension, behind one pluggable Executor (extension via chrome.scripting, or a Playwright CDP fallback).",
|
|
5
5
|
"author": "Mehmood Ur Rehman Qureshi",
|
|
6
6
|
"license": "MIT",
|