# Defense-in-Depth Score: invisible_playwright_mcp

**Repo:** https://github.com/feder-cr/invisible_playwright_mcp · **Commit:** `4139237b0abc6712bb44e4a3a2ff3dbeb77f1d80` (v0.70.9) · **Reviewed:** 2026-10-04
**What it is:** MCP server (plus optional web UI agent) that drives an anti-detection patched Firefox: navigate, click, type, read, upload, run page JavaScript.
**Category:** AI Assistants
**Scored configuration:** MCP server over stdio as installed by the README's plugin route (`uvx invisible-playwright-mcp`), no environment variables set, launched from the host's working directory.
**Agent surface (default):** code execution yes · filesystem write yes · network egress yes · external credentials opt-in · persistent memory yes · untrusted input yes · third party extensions no · sub agents no · external communication yes

## Score: 2.3 / 10.0 (Minimal)

| # | Criterion | S | C | D | B | Raw | Cap | Score | Confidence |
|---|---|---|---|---|---|---|---|---|---|
| C1 | Identity & least privilege | L1 | L0 | L1 | L1 | 0.17 | — | **0.17** | High |
| C2 | Approval gates | L1 | L1 | L2 | L1 | 0.30 | C2-POWERBYPASS | **0.25** | High |
| C3 | Tool & action scoping | L1 | L1 | L1 | L0 | 0.20 | — | **0.20** | High |
| C4 | Code-execution isolation | L2 | L2 | L2 | L1 | 0.45 | — | **0.45** | Low |
| C5 | Untrusted input blast radius | L0 | L0 | L0 | L0 | 0.00 | C5-WORSTCASE | **0.00** | High |
| C6 | Memory, context & configuration integrity | L0 | L0 | L1 | L1 | 0.10 | C6-REPOCONFIG | **0.10** | High |
| C7 | Third-party extensions | L2 | L2 | L0 | L1 | 0.35 | — | **0.35** | Medium |
| C8 | Secrets & sensitive-data protection | L1 | L2 | L2 | L2 | 0.42 | — | **0.42** | High |
| C9 | Audit & traceability | L0 | L0 | L0 | L0 | 0.00 | — | **0.00** | High |
| C10 | Limits & kill switch | L2 | L1 | L1 | L2 | 0.38 | — | **0.38** | High |


A capable stealth-browser MCP server with unusually careful file-upload scoping and password masking, but the dominant risk is that browser_evaluate runs arbitrary JavaScript while advertising itself as read-only, so hosts that auto-run read-only tools let an injected web page exfiltrate data or act on sites unattended. Navigation has no URL restrictions (file://, localhost and internal hosts are reachable), the server keeps no record of what it did, and it auto-loads a .env from the launch directory that can silently enable uploads, swap the proxy or engine binary, or expose the server over HTTP.

## Critical gaps
- browser_evaluate runs arbitrary model-written JavaScript but is annotated readOnlyHint=true, which the server documents as permission for hosts to run it without asking. (ASI02, ASI09, T2; C2) — [src/invisible_playwright_mcp/mcp/server.py:642](https://github.com/feder-cr/invisible_playwright_mcp/blob/4139237b0abc6712bb44e4a3a2ff3dbeb77f1d80/src/invisible_playwright_mcp/mcp/server.py#L642); [src/invisible_playwright_mcp/mcp/server.py:256](https://github.com/feder-cr/invisible_playwright_mcp/blob/4139237b0abc6712bb44e4a3a2ff3dbeb77f1d80/src/invisible_playwright_mcp/mcp/server.py#L256); [src/invisible_playwright_mcp/mcp/server.py:660](https://github.com/feder-cr/invisible_playwright_mcp/blob/4139237b0abc6712bb44e4a3a2ff3dbeb77f1d80/src/invisible_playwright_mcp/mcp/server.py#L660)
- Hijacked sessions can leak page data and act on live sites unattended: arbitrary JavaScript runs through a tool advertised as read-only, and navigate has no URL restrictions. (ASI01, LLM01, T6; C5) — [src/invisible_playwright_mcp/mcp/server.py:642](https://github.com/feder-cr/invisible_playwright_mcp/blob/4139237b0abc6712bb44e4a3a2ff3dbeb77f1d80/src/invisible_playwright_mcp/mcp/server.py#L642); [src/invisible_playwright_mcp/mcp/actions.py:1354](https://github.com/feder-cr/invisible_playwright_mcp/blob/4139237b0abc6712bb44e4a3a2ff3dbeb77f1d80/src/invisible_playwright_mcp/mcp/actions.py#L1354); [src/invisible_playwright_mcp/mcp/actions.py:125](https://github.com/feder-cr/invisible_playwright_mcp/blob/4139237b0abc6712bb44e4a3a2ff3dbeb77f1d80/src/invisible_playwright_mcp/mcp/actions.py#L125)
- A .env in the host's working directory is auto-loaded by the MCP server and can enable uploads, redirect all traffic through a proxy, choose the engine binary, or bind the unauthenticated HTTP transport to any interface. (ASI06, ASI04, T1; C6) — [src/invisible_playwright_mcp/cli.py:39](https://github.com/feder-cr/invisible_playwright_mcp/blob/4139237b0abc6712bb44e4a3a2ff3dbeb77f1d80/src/invisible_playwright_mcp/cli.py#L39); [src/invisible_playwright_mcp/cli.py:52](https://github.com/feder-cr/invisible_playwright_mcp/blob/4139237b0abc6712bb44e4a3a2ff3dbeb77f1d80/src/invisible_playwright_mcp/cli.py#L52); [src/invisible_playwright_mcp/cli.py:160](https://github.com/feder-cr/invisible_playwright_mcp/blob/4139237b0abc6712bb44e4a3a2ff3dbeb77f1d80/src/invisible_playwright_mcp/cli.py#L160)

## Criterion details

### C1 Identity & least privilege — 0.17 (high)

The server runs as the local user and holds no credentials of its own by default; the default browser is a fresh, throwaway profile with no logins, which is a real narrowing. But nothing checks what the browser is allowed to reach: the model can point browser_open at any directory as the profile (including a directory that already holds logins), choose any proxy, and navigate to any URL including file:// paths and local network services. A .env file in the directory the host starts the server from can widen the same settings silently.

- **S L1:** The default identity is an ephemeral browser with no profile, but the model can swap in any on-disk profile directory and proxy through browser_open arguments. — [src/invisible_playwright_mcp/mcp/server.py:318](https://github.com/feder-cr/invisible_playwright_mcp/blob/4139237b0abc6712bb44e4a3a2ff3dbeb77f1d80/src/invisible_playwright_mcp/mcp/server.py#L318); [src/invisible_playwright_mcp/mcp/plan.py:140](https://github.com/feder-cr/invisible_playwright_mcp/blob/4139237b0abc6712bb44e4a3a2ff3dbeb77f1d80/src/invisible_playwright_mcp/mcp/plan.py#L140) (verified)
  - *To reach the next level:* No identity is scoped to the job; profile and proxy are free model-chosen arguments rather than operator-set, checked values.
- **C L0:** No tool passes through any authorization check; navigate, evaluate and the profile argument all act with whatever the browser and OS user can reach. — [src/invisible_playwright_mcp/mcp/actions.py:125](https://github.com/feder-cr/invisible_playwright_mcp/blob/4139237b0abc6712bb44e4a3a2ff3dbeb77f1d80/src/invisible_playwright_mcp/mcp/actions.py#L125); searched `rg -n 'urlparse|scheme|file://'` in `src/invisible_playwright_mcp/mcp/actions.py src/invisible_playwright_mcp/mcp/server.py` → 0 hits (no URL scheme or host validation anywhere on the navigate path) (verified)
  - *To reach the next level:* No authorization layer on any tool path.
- **D L1:** Defaults are narrow (no profile, no proxy, uploads off) but are widened by a model argument or by a .env in the launch directory. — [src/invisible_playwright_mcp/cli.py:39](https://github.com/feder-cr/invisible_playwright_mcp/blob/4139237b0abc6712bb44e4a3a2ff3dbeb77f1d80/src/invisible_playwright_mcp/cli.py#L39); [src/invisible_playwright_mcp/cli.py:52](https://github.com/feder-cr/invisible_playwright_mcp/blob/4139237b0abc6712bb44e4a3a2ff3dbeb77f1d80/src/invisible_playwright_mcp/cli.py#L52); [src/invisible_playwright_mcp/mcp/plan.py:140](https://github.com/feder-cr/invisible_playwright_mcp/blob/4139237b0abc6712bb44e4a3a2ff3dbeb77f1d80/src/invisible_playwright_mcp/mcp/plan.py#L140) (verified)
  - *To reach the next level:* Widening should need an explicit operator change, not a model argument or a workspace file.
- **B L1:** A hijacked session can drive whatever web accounts a chosen profile is logged into and reach local files and local services through the browser. — [src/invisible_playwright_mcp/mcp/server.py:318](https://github.com/feder-cr/invisible_playwright_mcp/blob/4139237b0abc6712bb44e4a3a2ff3dbeb77f1d80/src/invisible_playwright_mcp/mcp/server.py#L318); [src/invisible_playwright_mcp/mcp/actions.py:125](https://github.com/feder-cr/invisible_playwright_mcp/blob/4139237b0abc6712bb44e4a3a2ff3dbeb77f1d80/src/invisible_playwright_mcp/mcp/actions.py#L125) (verified)
  - *To reach the next level:* No limit on which accounts, hosts or local resources the browser can reach.
- **Cap:** none

### C2 Approval gates — 0.25 (high)

As a tool server it does not own the approval prompt, so what matters is the risk signal it gives the host. Every tool carries explicit readOnly/destructive hints and clicking, typing and navigating are correctly marked destructive. But browser_evaluate, which runs arbitrary model-written JavaScript in the page, is marked read-only, and the code's own comment says read-only means a client may run it without asking. A regex refuses a handful of obvious page-changing calls but, by the authors' own statement, is not a boundary, and fetch(), location changes and similar calls pass. There is no server-side read-only mode or dry-run.

- **S L1:** Hints are present on all 14 tools, but the arbitrary-JavaScript tool browser_evaluate is advertised as readOnlyHint=true. — [src/invisible_playwright_mcp/mcp/server.py:642](https://github.com/feder-cr/invisible_playwright_mcp/blob/4139237b0abc6712bb44e4a3a2ff3dbeb77f1d80/src/invisible_playwright_mcp/mcp/server.py#L642); [src/invisible_playwright_mcp/mcp/server.py:256](https://github.com/feder-cr/invisible_playwright_mcp/blob/4139237b0abc6712bb44e4a3a2ff3dbeb77f1d80/src/invisible_playwright_mcp/mcp/server.py#L256); [src/invisible_playwright_mcp/mcp/server.py:276](https://github.com/feder-cr/invisible_playwright_mcp/blob/4139237b0abc6712bb44e4a3a2ff3dbeb77f1d80/src/invisible_playwright_mcp/mcp/server.py#L276) (verified)
  - *To reach the next level:* Every mutating or egress-capable tool, browser_evaluate included, must carry an accurate destructive hint.
- **C L1:** Navigate, click, type, select, upload and key presses are flagged; browser_evaluate is not, and it can navigate, post forms through fetch, or send data to any host. — [src/invisible_playwright_mcp/mcp/server.py:642](https://github.com/feder-cr/invisible_playwright_mcp/blob/4139237b0abc6712bb44e4a3a2ff3dbeb77f1d80/src/invisible_playwright_mcp/mcp/server.py#L642); [src/invisible_playwright_mcp/mcp/actions.py:1354](https://github.com/feder-cr/invisible_playwright_mcp/blob/4139237b0abc6712bb44e4a3a2ff3dbeb77f1d80/src/invisible_playwright_mcp/mcp/actions.py#L1354); [src/invisible_playwright_mcp/mcp/actions.py:1301](https://github.com/feder-cr/invisible_playwright_mcp/blob/4139237b0abc6712bb44e4a3a2ff3dbeb77f1d80/src/invisible_playwright_mcp/mcp/actions.py#L1301) (verified)
  - *To reach the next level:* All tools that can change state or send data off the machine must be flagged.
- **D L2:** The hints are hard-coded and nothing the model or page sends can change them. — [src/invisible_playwright_mcp/mcp/server.py:276](https://github.com/feder-cr/invisible_playwright_mcp/blob/4139237b0abc6712bb44e4a3a2ff3dbeb77f1d80/src/invisible_playwright_mcp/mcp/server.py#L276) (verified)
  - *To reach the next level:* No server-enforced read-only mode or confirmation step the host must complete.
- **B L1:** Wrongly approved actions are web form submissions, posts, sign-ups and purchases on live sites, with no preview or undo. — [src/invisible_playwright_mcp/mcp/server.py:559](https://github.com/feder-cr/invisible_playwright_mcp/blob/4139237b0abc6712bb44e4a3a2ff3dbeb77f1d80/src/invisible_playwright_mcp/mcp/server.py#L559) (verified)
  - *To reach the next level:* No dry-run or preview for destructive browser actions.
- **Cap:** C2-POWERBYPASS — browser_evaluate, the most powerful path (arbitrary JavaScript), is advertised read-only, which the server itself documents as permission for the host to run it without asking.

### C3 Tool & action scoping — 0.20 (high)

File upload is a model of careful scoping: it is off unless the operator lists directories, and each path is resolved, contained, refused through hidden directories, size-capped, and re-checked on the opened file. Everything else is general-purpose. browser_navigate accepts any URL with no scheme or host check, so file://, localhost and cloud-metadata addresses are not refused. browser_evaluate takes arbitrary JavaScript behind a denylist of a few method names. browser_open accepts any directory as a profile.

- **S L1:** The two general tools are raw passthrough (any URL) or denylist-filtered (JavaScript); only upload has allowlist validation. — [src/invisible_playwright_mcp/mcp/actions.py:125](https://github.com/feder-cr/invisible_playwright_mcp/blob/4139237b0abc6712bb44e4a3a2ff3dbeb77f1d80/src/invisible_playwright_mcp/mcp/actions.py#L125); searched `rg -n 'urlparse|scheme|file://'` in `src/invisible_playwright_mcp/mcp/actions.py src/invisible_playwright_mcp/mcp/server.py` → 0 hits (no URL scheme or host validation anywhere on the navigate path); [src/invisible_playwright_mcp/mcp/actions.py:1301](https://github.com/feder-cr/invisible_playwright_mcp/blob/4139237b0abc6712bb44e4a3a2ff3dbeb77f1d80/src/invisible_playwright_mcp/mcp/actions.py#L1301) (verified)
  - *To reach the next level:* URL allowlist or at least scheme and internal-address blocking on navigate, and a narrow replacement for arbitrary JavaScript.
- **C L1:** Upload validates thoroughly; proxy URLs are parsed; navigate, evaluate, type, profile paths are unchecked. — [src/invisible_playwright_mcp/mcp/actions.py:984](https://github.com/feder-cr/invisible_playwright_mcp/blob/4139237b0abc6712bb44e4a3a2ff3dbeb77f1d80/src/invisible_playwright_mcp/mcp/actions.py#L984); [src/invisible_playwright_mcp/mcp/actions.py:1042](https://github.com/feder-cr/invisible_playwright_mcp/blob/4139237b0abc6712bb44e4a3a2ff3dbeb77f1d80/src/invisible_playwright_mcp/mcp/actions.py#L1042); [src/invisible_playwright_mcp/mcp/actions.py:1052](https://github.com/feder-cr/invisible_playwright_mcp/blob/4139237b0abc6712bb44e4a3a2ff3dbeb77f1d80/src/invisible_playwright_mcp/mcp/actions.py#L1052); [src/invisible_playwright_mcp/mcp/proxy.py:16](https://github.com/feder-cr/invisible_playwright_mcp/blob/4139237b0abc6712bb44e4a3a2ff3dbeb77f1d80/src/invisible_playwright_mcp/mcp/proxy.py#L16) (verified)
  - *To reach the next level:* Most tools should validate their arguments in code.
- **D L1:** Uploads are off by default, but navigate, evaluate, typing and clicking are always on and cannot be disabled individually. — [src/invisible_playwright_mcp/mcp/actions.py:977](https://github.com/feder-cr/invisible_playwright_mcp/blob/4139237b0abc6712bb44e4a3a2ff3dbeb77f1d80/src/invisible_playwright_mcp/mcp/actions.py#L977); [src/invisible_playwright_mcp/mcp/actions.py:909](https://github.com/feder-cr/invisible_playwright_mcp/blob/4139237b0abc6712bb44e4a3a2ff3dbeb77f1d80/src/invisible_playwright_mcp/mcp/actions.py#L909) (verified)
  - *To reach the next level:* A read-only tool set by default with acting tools opt-in.
- **B L0:** A misused navigate/evaluate reaches any web host, local network service, and file:// path the browser will open. — [src/invisible_playwright_mcp/mcp/actions.py:125](https://github.com/feder-cr/invisible_playwright_mcp/blob/4139237b0abc6712bb44e4a3a2ff3dbeb77f1d80/src/invisible_playwright_mcp/mcp/actions.py#L125); searched `rg -n 'urlparse|scheme|file://'` in `src/invisible_playwright_mcp/mcp/actions.py src/invisible_playwright_mcp/mcp/server.py` → 0 hits (no URL scheme or host validation anywhere on the navigate path) (verified)
  - *To reach the next level:* Tools should be scoped to declared hosts or at least exclude local and internal addresses.
- **Cap:** none

### C4 Code-execution isolation — 0.45 (low)

Model-written JavaScript runs through browser_evaluate inside the page of a patched Firefox. The only containment is the browser's own content-process sandbox and same-origin rules, which this repository neither configures nor verifies; the regex filter on the script is explicitly described by its authors as not a sandbox. The browser process itself runs as the user with the server's environment, full network, and whatever logins the chosen profile holds.

- **S L2:** Model JavaScript executes in Firefox's page context, contained by the browser's content sandbox (third-party behaviour, not configured here). — [src/invisible_playwright_mcp/mcp/actions.py:1354](https://github.com/feder-cr/invisible_playwright_mcp/blob/4139237b0abc6712bb44e4a3a2ff3dbeb77f1d80/src/invisible_playwright_mcp/mcp/actions.py#L1354); searched `rg -n -i sandbox` in `src/invisible_playwright_mcp/` → 2 hits (one comment about an engine error line, one comment stating the evaluate filter is NOT a sandbox) (inferred)
  - *To reach the next level:* No isolation configured by the project itself; the browser process is not separately sandboxed.
- **C L2:** All model-supplied JavaScript goes through page.evaluate in the page context; there is no other code-execution path. — [src/invisible_playwright_mcp/mcp/actions.py:1354](https://github.com/feder-cr/invisible_playwright_mcp/blob/4139237b0abc6712bb44e4a3a2ff3dbeb77f1d80/src/invisible_playwright_mcp/mcp/actions.py#L1354); [src/invisible_playwright_mcp/mcp/server.py:660](https://github.com/feder-cr/invisible_playwright_mcp/blob/4139237b0abc6712bb44e4a3a2ff3dbeb77f1d80/src/invisible_playwright_mcp/mcp/server.py#L660) (inferred)
  - *To reach the next level:* Coverage depends entirely on the browser's own sandbox, which the project does not verify.
- **D L2:** Nothing in this repo disables the browser sandbox, but nothing pins or verifies it either. — searched `rg -n -i sandbox` in `src/invisible_playwright_mcp/` → 2 hits (one comment about an engine error line, one comment stating the evaluate filter is NOT a sandbox) (inferred)
  - *To reach the next level:* The sandbox policy is not defined or checked by the project.
- **B L1:** A script inherits the page origin's cookies and session, and the browser has unrestricted network egress. — [src/invisible_playwright_mcp/mcp/actions.py:125](https://github.com/feder-cr/invisible_playwright_mcp/blob/4139237b0abc6712bb44e4a3a2ff3dbeb77f1d80/src/invisible_playwright_mcp/mcp/actions.py#L125); [src/invisible_playwright_mcp/mcp/plan.py:140](https://github.com/feder-cr/invisible_playwright_mcp/blob/4139237b0abc6712bb44e4a3a2ff3dbeb77f1d80/src/invisible_playwright_mcp/mcp/plan.py#L140) (verified)
  - *To reach the next level:* No egress restriction or credential separation for the page context.
- **Cap:** none

### C5 Untrusted input blast radius — 0.00 (high)

The server reads arbitrary web pages and returns their text, HTML and snapshot to the model as plain results, with no untrusted marker and mixed with the server's own instructions. Nothing drops a leg of the Rule of Two: the same session reads hostile pages, can hold logged-in sessions or local files, and can send data anywhere through navigate or through browser_evaluate, which is advertised read-only so many hosts will run it unprompted. The server's own instructions also coach the model to use throwaway-mail sites to get through email verification.

- **S L0:** Server instructions and tool descriptions contain directives to the model, and page content is returned without provenance flags. — [src/invisible_playwright_mcp/mcp/server.py:154](https://github.com/feder-cr/invisible_playwright_mcp/blob/4139237b0abc6712bb44e4a3a2ff3dbeb77f1d80/src/invisible_playwright_mcp/mcp/server.py#L154); [src/invisible_playwright_mcp/mcp/server.py:210](https://github.com/feder-cr/invisible_playwright_mcp/blob/4139237b0abc6712bb44e4a3a2ff3dbeb77f1d80/src/invisible_playwright_mcp/mcp/server.py#L210); searched `rg -n -S 'untrusted|provenance'` in `src/invisible_playwright_mcp/mcp/` → 3 hits (all three refer to synthetic DOM events, none marks tool output as untrusted content) (verified)
  - *To reach the next level:* Structured outputs separating page content from server metadata, with an untrusted flag.
- **C L0:** No untrusted source is distinguished. — searched `rg -n -S 'untrusted|provenance'` in `src/invisible_playwright_mcp/mcp/` → 3 hits (all three refer to synthetic DOM events, none marks tool output as untrusted content) (verified)
  - *To reach the next level:* Every page-derived output should be marked as untrusted content.
- **D L0:** No control exists to be on by default. — searched `rg -n -S 'untrusted|provenance'` in `src/invisible_playwright_mcp/mcp/` → 3 hits (all three refer to synthetic DOM events, none marks tool output as untrusted content) (verified)
  - *To reach the next level:* Offer a mode that drops a Rule-of-Two leg (no egress or read-only).
- **B L0:** An injected page can make the model read session data from the page and send it off through browser_evaluate fetch() or navigate, and can trigger posts or purchases, all on hosts that auto-run read-only tools. — [src/invisible_playwright_mcp/mcp/server.py:642](https://github.com/feder-cr/invisible_playwright_mcp/blob/4139237b0abc6712bb44e4a3a2ff3dbeb77f1d80/src/invisible_playwright_mcp/mcp/server.py#L642); [src/invisible_playwright_mcp/mcp/actions.py:1354](https://github.com/feder-cr/invisible_playwright_mcp/blob/4139237b0abc6712bb44e4a3a2ff3dbeb77f1d80/src/invisible_playwright_mcp/mcp/actions.py#L1354); [src/invisible_playwright_mcp/mcp/actions.py:125](https://github.com/feder-cr/invisible_playwright_mcp/blob/4139237b0abc6712bb44e4a3a2ff3dbeb77f1d80/src/invisible_playwright_mcp/mcp/actions.py#L125) (verified)
  - *To reach the next level:* Some leg of leak-plus-act must require approval enforced by the server.
- **Cap:** C5-WORSTCASE — Worst case (B L0): a hijacked agent can leak data and take irreversible actions unattended.

### C6 Memory, context & configuration integrity — 0.10 (high)

The MCP server reads a .env file from whatever directory the host starts it in, before serving, and applies every variable it finds. Hosts commonly start servers in the project directory, so a cloned repository's .env can silently turn on uploads from chosen directories, route all browsing through an attacker's proxy, pick the browser profile or engine binary, or switch the server to HTTP on any interface with no authentication. Separately, the proxy and profile a model chooses are written to a session file and reused by the next browser_open with no arguments, and all standalone clients share the same default file.

- **S L0:** A workspace .env can set security-relevant settings (upload dirs, proxy, binary, transport host) with no prompt. — [src/invisible_playwright_mcp/cli.py:39](https://github.com/feder-cr/invisible_playwright_mcp/blob/4139237b0abc6712bb44e4a3a2ff3dbeb77f1d80/src/invisible_playwright_mcp/cli.py#L39); [src/invisible_playwright_mcp/cli.py:52](https://github.com/feder-cr/invisible_playwright_mcp/blob/4139237b0abc6712bb44e4a3a2ff3dbeb77f1d80/src/invisible_playwright_mcp/cli.py#L52); [src/invisible_playwright_mcp/cli.py:160](https://github.com/feder-cr/invisible_playwright_mcp/blob/4139237b0abc6712bb44e4a3a2ff3dbeb77f1d80/src/invisible_playwright_mcp/cli.py#L160); [src/invisible_playwright_mcp/mcp/actions.py:909](https://github.com/feder-cr/invisible_playwright_mcp/blob/4139237b0abc6712bb44e4a3a2ff3dbeb77f1d80/src/invisible_playwright_mcp/mcp/actions.py#L909); [src/invisible_playwright_mcp/mcp/server.py:674](https://github.com/feder-cr/invisible_playwright_mcp/blob/4139237b0abc6712bb44e4a3a2ff3dbeb77f1d80/src/invisible_playwright_mcp/mcp/server.py#L674) (verified)
  - *To reach the next level:* Load configuration only from user scope or an explicit flag, never the working directory.
- **C L0:** Neither the .env path nor the persisted session file is validated. — [src/invisible_playwright_mcp/cli.py:39](https://github.com/feder-cr/invisible_playwright_mcp/blob/4139237b0abc6712bb44e4a3a2ff3dbeb77f1d80/src/invisible_playwright_mcp/cli.py#L39); [src/invisible_playwright_mcp/mcp/work.py:288](https://github.com/feder-cr/invisible_playwright_mcp/blob/4139237b0abc6712bb44e4a3a2ff3dbeb77f1d80/src/invisible_playwright_mcp/mcp/work.py#L288) (verified)
  - *To reach the next level:* Both auto-loaded config and the persisted identity should be controlled.
- **D L1:** Interface-spawned servers get a per-conversation session id, but standalone clients all share the default session file. — [src/invisible_playwright_mcp/mcp/server.py:81](https://github.com/feder-cr/invisible_playwright_mcp/blob/4139237b0abc6712bb44e4a3a2ff3dbeb77f1d80/src/invisible_playwright_mcp/mcp/server.py#L81) (verified)
  - *To reach the next level:* Per-client session namespaces by default.
- **B L1:** A poisoned proxy or profile choice persists across the user's sessions and shapes every later browse. — [src/invisible_playwright_mcp/mcp/work.py:86](https://github.com/feder-cr/invisible_playwright_mcp/blob/4139237b0abc6712bb44e4a3a2ff3dbeb77f1d80/src/invisible_playwright_mcp/mcp/work.py#L86); [src/invisible_playwright_mcp/mcp/work.py:288](https://github.com/feder-cr/invisible_playwright_mcp/blob/4139237b0abc6712bb44e4a3a2ff3dbeb77f1d80/src/invisible_playwright_mcp/mcp/work.py#L288) (verified)
  - *To reach the next level:* Persisted identity should be shown for confirmation or be session-scoped.
- **Cap:** C6-REPOCONFIG — A .env in the launch directory is applied before serving and can enable uploads, redirect the proxy, choose the engine binary and expose the server over HTTP (cli.py:39-52,160).

### C7 Third-party extensions — 0.35 (medium)

The server has no plugin or extension mechanism, but at startup it automatically downloads and then runs a roughly 250 MB patched Firefox build. The download and its check against a pinned 'seal' live in the invisible-playwright/invisible-core dependencies, so the verification can't be confirmed from this repository. The binary path can also be set through STEALTHFOX_BINARY, including from a workspace .env, and the browser runs as the user with the server's environment.

- **S L2:** The engine is described as pinned and verified against a seal, but that check is in a dependency not reviewed here. — [src/invisible_playwright_mcp/engine.py:166](https://github.com/feder-cr/invisible_playwright_mcp/blob/4139237b0abc6712bb44e4a3a2ff3dbeb77f1d80/src/invisible_playwright_mcp/engine.py#L166); [src/invisible_playwright_mcp/engine.py:29](https://github.com/feder-cr/invisible_playwright_mcp/blob/4139237b0abc6712bb44e4a3a2ff3dbeb77f1d80/src/invisible_playwright_mcp/engine.py#L29) (inferred)
  - *To reach the next level:* Integrity verification visible and enforced in this codebase.
- **C L2:** The engine is the only runtime-loaded executable and goes through the same fetch/verify path. — [src/invisible_playwright_mcp/engine.py:166](https://github.com/feder-cr/invisible_playwright_mcp/blob/4139237b0abc6712bb44e4a3a2ff3dbeb77f1d80/src/invisible_playwright_mcp/engine.py#L166); [src/invisible_playwright_mcp/mcp/plan.py:221](https://github.com/feder-cr/invisible_playwright_mcp/blob/4139237b0abc6712bb44e4a3a2ff3dbeb77f1d80/src/invisible_playwright_mcp/mcp/plan.py#L221) (inferred)
  - *To reach the next level:* Verification of an operator- or workspace-named binary cannot be confirmed here.
- **D L0:** The download starts automatically at process start, and a workspace .env can name the binary to run. — [src/invisible_playwright_mcp/mcp/server.py:670](https://github.com/feder-cr/invisible_playwright_mcp/blob/4139237b0abc6712bb44e4a3a2ff3dbeb77f1d80/src/invisible_playwright_mcp/mcp/server.py#L670); [src/invisible_playwright_mcp/mcp/plan.py:221](https://github.com/feder-cr/invisible_playwright_mcp/blob/4139237b0abc6712bb44e4a3a2ff3dbeb77f1d80/src/invisible_playwright_mcp/mcp/plan.py#L221); [src/invisible_playwright_mcp/cli.py:52](https://github.com/feder-cr/invisible_playwright_mcp/blob/4139237b0abc6712bb44e4a3a2ff3dbeb77f1d80/src/invisible_playwright_mcp/cli.py#L52) (verified)
  - *To reach the next level:* Only user or admin scope should be able to choose the engine binary, with the exact artifact shown.
- **B L1:** The engine runs as a separate process under the same user with the inherited environment (the OpenRouter key is stripped). — [src/invisible_playwright_mcp/cli.py:191](https://github.com/feder-cr/invisible_playwright_mcp/blob/4139237b0abc6712bb44e4a3a2ff3dbeb77f1d80/src/invisible_playwright_mcp/cli.py#L191) (verified)
  - *To reach the next level:* Run the engine with a scrubbed environment and a sandbox.
- **Cap:** none

### C8 Secrets & sensitive-data protection — 0.42 (high)

There is good care around page secrets: password and one-time-code fields are masked in snapshots, HTML reads and typing replies, so a filled password is not echoed into the conversation, and the server drops any OpenRouter key from its environment before launching the browser. There is no telemetry and no logging. Weak spots: proxy credentials are written in plaintext into the session file with default permissions, and browser_evaluate and read_text can still read secret values directly.

- **S L1:** Secret form fields are masked on the main model-bound outputs, but stored proxy credentials are plaintext with default file permissions. — [src/invisible_playwright_mcp/mcp/clean.py:410](https://github.com/feder-cr/invisible_playwright_mcp/blob/4139237b0abc6712bb44e4a3a2ff3dbeb77f1d80/src/invisible_playwright_mcp/mcp/clean.py#L410); [src/invisible_playwright_mcp/mcp/actions.py:396](https://github.com/feder-cr/invisible_playwright_mcp/blob/4139237b0abc6712bb44e4a3a2ff3dbeb77f1d80/src/invisible_playwright_mcp/mcp/actions.py#L396); [src/invisible_playwright_mcp/mcp/proxy.py:27](https://github.com/feder-cr/invisible_playwright_mcp/blob/4139237b0abc6712bb44e4a3a2ff3dbeb77f1d80/src/invisible_playwright_mcp/mcp/proxy.py#L27); [src/invisible_playwright_mcp/storage.py:163](https://github.com/feder-cr/invisible_playwright_mcp/blob/4139237b0abc6712bb44e4a3a2ff3dbeb77f1d80/src/invisible_playwright_mcp/storage.py#L163) (verified)
  - *To reach the next level:* Restrictive permissions or keychain storage for persisted credentials.
- **C L2:** Masking covers snapshot, HTML and typing outputs and the engine's environment; evaluate, read_text and the session file are not covered. — [src/invisible_playwright_mcp/mcp/clean.py:410](https://github.com/feder-cr/invisible_playwright_mcp/blob/4139237b0abc6712bb44e4a3a2ff3dbeb77f1d80/src/invisible_playwright_mcp/mcp/clean.py#L410); [src/invisible_playwright_mcp/mcp/actions.py:396](https://github.com/feder-cr/invisible_playwright_mcp/blob/4139237b0abc6712bb44e4a3a2ff3dbeb77f1d80/src/invisible_playwright_mcp/mcp/actions.py#L396); [src/invisible_playwright_mcp/cli.py:191](https://github.com/feder-cr/invisible_playwright_mcp/blob/4139237b0abc6712bb44e4a3a2ff3dbeb77f1d80/src/invisible_playwright_mcp/cli.py#L191); [src/invisible_playwright_mcp/mcp/work.py:288](https://github.com/feder-cr/invisible_playwright_mcp/blob/4139237b0abc6712bb44e4a3a2ff3dbeb77f1d80/src/invisible_playwright_mcp/mcp/work.py#L288) (verified)
  - *To reach the next level:* Cover every model-bound output and the persisted session file.
- **D L2:** Masking is always on and no telemetry exists. — [src/invisible_playwright_mcp/mcp/clean.py:410](https://github.com/feder-cr/invisible_playwright_mcp/blob/4139237b0abc6712bb44e4a3a2ff3dbeb77f1d80/src/invisible_playwright_mcp/mcp/clean.py#L410); searched `rg -n 'logging|logger|log\.'` in `src/invisible_playwright_mcp/mcp/` → 0 hits (the MCP server package has no logging of any kind) (verified)
  - *To reach the next level:* Stored transcripts and session files should be minimised or encrypted by default.
- **B L2:** Exposed secrets are proxy credentials and, with a profile, website session cookies: scoped but long-lived. — [src/invisible_playwright_mcp/mcp/proxy.py:27](https://github.com/feder-cr/invisible_playwright_mcp/blob/4139237b0abc6712bb44e4a3a2ff3dbeb77f1d80/src/invisible_playwright_mcp/mcp/proxy.py#L27); [src/invisible_playwright_mcp/mcp/plan.py:140](https://github.com/feder-cr/invisible_playwright_mcp/blob/4139237b0abc6712bb44e4a3a2ff3dbeb77f1d80/src/invisible_playwright_mcp/mcp/plan.py#L140) (verified)
  - *To reach the next level:* Short-lived, rotatable credentials.
- **Cap:** none

### C9 Audit & traceability — 0.00 (high)

The MCP server keeps no record of what it did: no tool-call log, no audit file, no logging calls at all in the server package. The only persisted file records who the main browser is (seed, proxy, profile), not its actions. Any record of activity exists only in the host's transcript, which doesn't count for the server.

- **S L0:** Tool calls are not recorded. — searched `rg -n 'logging|logger|log\.'` in `src/invisible_playwright_mcp/mcp/` → 0 hits (the MCP server package has no logging of any kind) (verified)
  - *To reach the next level:* A structured record of each tool call with arguments, status and timestamp.
- **C L0:** Nothing is recorded. — searched `rg -n 'logging|logger|log\.'` in `src/invisible_playwright_mcp/mcp/` → 0 hits (the MCP server package has no logging of any kind) (verified)
  - *To reach the next level:* Record every tool call.
- **D L0:** No audit facility exists to enable. — searched `rg -n 'logging|logger|log\.'` in `src/invisible_playwright_mcp/mcp/` → 0 hits (the MCP server package has no logging of any kind) (verified)
  - *To reach the next level:* An on-by-default audit log outside the workspace.
- **B L0:** Nothing is recorded, so nothing survives an incident. — searched `rg -n 'logging|logger|log\.'` in `src/invisible_playwright_mcp/mcp/` → 0 hits (the MCP server package has no logging of any kind) (verified)
  - *To reach the next level:* Durable per-action records.
- **Cap:** none

### C10 Limits & kill switch — 0.38 (high)

The server bounds most single operations: navigation waits at most 45 seconds, element actions 15 seconds, and text and JavaScript results are capped at 6,000 characters. Some operations are open-ended: snapshots are uncapped by default, press-and-hold duration is whatever the model asks, and browser_evaluate has no timeout. Closing the stdio connection closes the browsers and cancels background typing. There are no rate limits.

- **S L2:** Server-enforced timeouts and output caps exist on most operations. — [src/invisible_playwright_mcp/mcp/actions.py:97](https://github.com/feder-cr/invisible_playwright_mcp/blob/4139237b0abc6712bb44e4a3a2ff3dbeb77f1d80/src/invisible_playwright_mcp/mcp/actions.py#L97); [src/invisible_playwright_mcp/mcp/actions.py:649](https://github.com/feder-cr/invisible_playwright_mcp/blob/4139237b0abc6712bb44e4a3a2ff3dbeb77f1d80/src/invisible_playwright_mcp/mcp/actions.py#L649); [src/invisible_playwright_mcp/mcp/actions.py:33](https://github.com/feder-cr/invisible_playwright_mcp/blob/4139237b0abc6712bb44e4a3a2ff3dbeb77f1d80/src/invisible_playwright_mcp/mcp/actions.py#L33) (verified)
  - *To reach the next level:* Caps on every operation plus rate or concurrency limits.
- **C L1:** Snapshot output, hold duration and evaluate run time are unbounded. — [src/invisible_playwright_mcp/mcp/actions.py:461](https://github.com/feder-cr/invisible_playwright_mcp/blob/4139237b0abc6712bb44e4a3a2ff3dbeb77f1d80/src/invisible_playwright_mcp/mcp/actions.py#L461); [src/invisible_playwright_mcp/mcp/actions.py:707](https://github.com/feder-cr/invisible_playwright_mcp/blob/4139237b0abc6712bb44e4a3a2ff3dbeb77f1d80/src/invisible_playwright_mcp/mcp/actions.py#L707); [src/invisible_playwright_mcp/mcp/actions.py:1354](https://github.com/feder-cr/invisible_playwright_mcp/blob/4139237b0abc6712bb44e4a3a2ff3dbeb77f1d80/src/invisible_playwright_mcp/mcp/actions.py#L1354) (verified)
  - *To reach the next level:* Every tool should carry a server-enforced bound.
- **D L1:** Defaults are sensible but the model can raise hold_seconds and choose an uncapped snapshot. — [src/invisible_playwright_mcp/mcp/actions.py:707](https://github.com/feder-cr/invisible_playwright_mcp/blob/4139237b0abc6712bb44e4a3a2ff3dbeb77f1d80/src/invisible_playwright_mcp/mcp/actions.py#L707); [src/invisible_playwright_mcp/mcp/actions.py:461](https://github.com/feder-cr/invisible_playwright_mcp/blob/4139237b0abc6712bb44e4a3a2ff3dbeb77f1d80/src/invisible_playwright_mcp/mcp/actions.py#L461) (verified)
  - *To reach the next level:* The model should not be able to raise its own bounds.
- **B L2:** Stopping the client closes every browser and cancels background typing; in-flight page scripts are not interrupted. — [src/invisible_playwright_mcp/mcp/server.py:129](https://github.com/feder-cr/invisible_playwright_mcp/blob/4139237b0abc6712bb44e4a3a2ff3dbeb77f1d80/src/invisible_playwright_mcp/mcp/server.py#L129); [src/invisible_playwright_mcp/mcp/work.py:458](https://github.com/feder-cr/invisible_playwright_mcp/blob/4139237b0abc6712bb44e4a3a2ff3dbeb77f1d80/src/invisible_playwright_mcp/mcp/work.py#L458) (verified)
  - *To reach the next level:* Cancellation that interrupts in-flight evaluate calls and tight per-session ceilings.
- **Cap:** none

## Rule-of-Two check
[A] untrusted input: Arbitrary web page text/HTML returned to the model (src/invisible_playwright_mcp/mcp/actions.py:125, browser_read_text/read_html/snapshot) · [B] sensitive data/systems: Logged-in sessions in a model-chosen profile dir (mcp/plan.py:140), file:// paths via navigate, upload dirs (mcp/actions.py:909) · [C] state change / egress: browser_navigate to any URL (mcp/actions.py:125) and browser_evaluate JavaScript incl. fetch, advertised read-only (mcp/server.py:642) · Same default session? Yes

## Highest-impact improvements
1. Mark browser_evaluate destructive (readOnlyHint=false, destructiveHint=true) so hosts confirm it. — C2 S L1→L2, +0.075 before caps (Playbook 5)
2. Stop loading .env from the working directory in MCP-server mode; read it only for the ui subcommand or from user scope. — C6 S L0→L2, +0.150 before caps (Playbook 2)
3. Validate navigate URLs: allow only http/https, block loopback, private ranges and 169.254.169.254, recheck after redirects. — C3 S L1→L2, +0.075 before caps (Playbook 3)
4. Write a structured per-tool-call JSONL audit log outside the workspace. — C9 S L0→L2, +0.150 before caps (Playbook 1 step 3)
5. Wrap page-derived outputs in structured results with source URL and an untrusted flag, and offer a no-egress/read-only mode. — C5 S L0→L2, +0.150 before caps (Playbook 1)

## Re-audit log
- No changes.

## Limitations
- Static source review of the pinned commit only; nothing was executed, installed, or probed.
- Scored the MCP server (README option 1). The standalone `ui` agent (OpenRouter key, agent loop, unauthenticated local web UI on 127.0.0.1:8765) was not scored; it strips the model key from the server's environment (runner.py) but otherwise shares the server's tools.
- Engine download and seal verification, and Firefox's own content sandbox, live in third-party dependencies (invisible-playwright, invisible-core, Firefox) and were not reviewed; C4 and C7 are INFERRED-heavy.
- Whether the patched Firefox opens file:// URLs and real user Firefox profiles is inferred from stock Playwright-Firefox behaviour, not verified.
- No text aimed at AI reviewers or auditors was found in the repository.
