cdpilot 0.1.1 → 0.1.2

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/README.md CHANGED
@@ -261,6 +261,16 @@ Future paid offerings:
261
261
  - **Team dashboard** — Shared sessions, audit logs, usage analytics
262
262
  - **Priority support** — Direct help for enterprise integrations
263
263
 
264
+ ## Security
265
+
266
+ - **Isolated browser profile** — cdpilot runs in `~/.cdpilot/profile`, separate from your daily browser. Your cookies, passwords, and history are never exposed.
267
+ - **No arbitrary file access** — MCP screenshot filenames are sanitized and restricted to the screenshots directory. Path traversal is blocked.
268
+ - **Safe CSS selectors** — All selectors passed to `querySelector` are JSON-escaped to prevent injection.
269
+ - **No network exposure** — CDP listens on `127.0.0.1` only. Remote connections are not possible by default.
270
+ - **No dependencies** — Zero npm/Python runtime dependencies means zero supply-chain attack surface.
271
+
272
+ Found a vulnerability? Please email the maintainer directly instead of opening a public issue.
273
+
264
274
  ## Contributing
265
275
 
266
276
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cdpilot",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Zero-dependency browser automation from your terminal. One command, full control.",
5
5
  "bin": {
6
6
  "cdpilot": "./bin/cdpilot.js",
@@ -15,7 +15,7 @@
15
15
  "devtools", "headless", "screenshot", "testing", "web-scraping",
16
16
  "ai-agent", "mcp", "claude", "brave"
17
17
  ],
18
- "author": "",
18
+ "author": "Mehmet Nadir",
19
19
  "license": "MIT",
20
20
  "repository": {
21
21
  "type": "git",
@@ -26,7 +26,7 @@
26
26
  },
27
27
  "files": [
28
28
  "bin/",
29
- "src/",
29
+ "src/cdpilot.py",
30
30
  "README.md",
31
31
  "LICENSE"
32
32
  ]
package/src/cdpilot.py CHANGED
@@ -11,10 +11,10 @@ Usage:
11
11
  Environment:
12
12
  CDP_PORT CDP debugging port (default: 9222)
13
13
  CHROME_BIN Browser binary path (auto-detected if not set)
14
- BROWSERCTL_PROFILE Isolated browser profile directory
14
+ CDPILOT_PROFILE Isolated browser profile directory
15
15
  """
16
16
 
17
- __version__ = "0.1.1"
17
+ __version__ = "0.1.2"
18
18
 
19
19
  import asyncio
20
20
  import json
@@ -903,9 +903,10 @@ async def cmd_eval(js_code):
903
903
 
904
904
  async def cmd_click(selector):
905
905
  ws, _ = get_page_ws()
906
+ safe_sel = json.dumps(selector)
906
907
  js = f"""(function() {{
907
- const el = document.querySelector('{selector}');
908
- if (!el) return 'Not found: {selector}';
908
+ const el = document.querySelector({safe_sel});
909
+ if (!el) return 'Not found: ' + {safe_sel};
909
910
  el.scrollIntoView({{behavior:'smooth', block:'center'}});
910
911
  el.click();
911
912
  return 'Clicked: ' + el.tagName + ' ' + (el.textContent || '').substring(0, 60).trim();
@@ -917,10 +918,11 @@ async def cmd_click(selector):
917
918
  async def cmd_fill(selector, value):
918
919
  """Fill an input field (React/Vue compatible)."""
919
920
  ws, _ = get_page_ws()
921
+ safe_sel = json.dumps(selector)
920
922
  safe_value = json.dumps(value)
921
923
  js = f"""(function() {{
922
- const el = document.querySelector('{selector}');
923
- if (!el) return 'Not found: {selector}';
924
+ const el = document.querySelector({safe_sel});
925
+ if (!el) return 'Not found: ' + {safe_sel};
924
926
  el.focus();
925
927
  const nativeSet = Object.getOwnPropertyDescriptor(
926
928
  window.HTMLInputElement.prototype, 'value'
@@ -936,9 +938,10 @@ async def cmd_fill(selector, value):
936
938
 
937
939
  async def cmd_submit(selector="form"):
938
940
  ws, _ = get_page_ws()
941
+ safe_sel = json.dumps(selector)
939
942
  js = f"""(function() {{
940
- const form = document.querySelector('{selector}');
941
- if (!form) return 'Form not found: {selector}';
943
+ const form = document.querySelector({safe_sel});
944
+ if (!form) return 'Form not found: ' + {safe_sel};
942
945
  const btn = form.querySelector('button[type=submit], input[type=submit], button:last-of-type');
943
946
  if (btn) {{ btn.click(); return 'Submit clicked: ' + btn.textContent.trim(); }}
944
947
  form.submit();
@@ -950,15 +953,16 @@ async def cmd_submit(selector="form"):
950
953
 
951
954
  async def cmd_wait(selector, timeout=5):
952
955
  ws, _ = get_page_ws()
956
+ safe_sel = json.dumps(selector)
953
957
  js = f"""new Promise((resolve) => {{
954
- const el = document.querySelector('{selector}');
958
+ const el = document.querySelector({safe_sel});
955
959
  if (el) return resolve('Found: ' + el.tagName + ' ' + (el.textContent||'').substring(0,60).trim());
956
960
  const obs = new MutationObserver(() => {{
957
- const el = document.querySelector('{selector}');
961
+ const el = document.querySelector({safe_sel});
958
962
  if (el) {{ obs.disconnect(); resolve('Found: ' + el.tagName + ' ' + (el.textContent||'').substring(0,60).trim()); }}
959
963
  }});
960
964
  obs.observe(document.body, {{childList:true, subtree:true}});
961
- setTimeout(() => {{ obs.disconnect(); resolve('Timeout: {selector} not found ({timeout}s)'); }}, {int(timeout)*1000});
965
+ setTimeout(() => {{ obs.disconnect(); resolve('Timeout: ' + {safe_sel} + ' not found ({timeout}s)'); }}, {int(timeout)*1000});
962
966
  }})"""
963
967
  r = await cdp_send(ws, [(1, "Runtime.evaluate", {"expression": js, "returnByValue": True, "awaitPromise": True})])
964
968
  print(r.get(1, {}).get("result", {}).get("value", "?"))
@@ -2270,11 +2274,20 @@ class MCPServer:
2270
2274
  else:
2271
2275
  return {"jsonrpc": "2.0", "id": req_id, "error": {"code": -32601, "message": f"Method not found: {method}"}}
2272
2276
 
2277
+ @staticmethod
2278
+ def _safe_filename(name):
2279
+ import re
2280
+ base = os.path.basename(name)
2281
+ base = re.sub(r'[^\w.\-]', '_', base)
2282
+ if not base.lower().endswith('.png'):
2283
+ base += '.png'
2284
+ return os.path.join(SCREENSHOT_DIR, base)
2285
+
2273
2286
  def _execute_tool(self, req_id, tool_name, args):
2274
2287
  import io, subprocess
2275
2288
  tool_map = {
2276
2289
  "browser_navigate": lambda a: ["go", a.get("url", "")],
2277
- "browser_screenshot": lambda a: ["shot"] + ([a["filename"]] if a.get("filename") else []),
2290
+ "browser_screenshot": lambda a: ["shot"] + ([self._safe_filename(a["filename"])] if a.get("filename") else []),
2278
2291
  "browser_click": lambda a: ["click", a.get("selector", "")],
2279
2292
  "browser_type": lambda a: ["type", a.get("selector", ""), a.get("text", "")],
2280
2293
  "browser_content": lambda a: ["content"],