| Описание | Summary
pptr-mcp exposes a single MCP tool named execute that accepts a user-supplied JavaScript string and runs it on the MCP server host. Although the code runs inside a Node.js vm sandbox, the sandbox is explicitly documented as a convenience layer—not a security boundary—and the tool grants full access to a live Puppeteer Browser instance.
Using MCP Inspector to connect to a locally built instance of the server, three attack primitives were confirmed:
Arbitrary JavaScript execution on the server process.
Unrestricted browser navigation and page content extraction (any URL reachable by Chrome).
Local filesystem read via the file:// protocol through the controlled browser.
Together, these demonstrate that any party capable of invoking the execute tool (e.g., an LLM agent, prompt-injection payload, or compromised MCP client) can execute attacker-controlled logic on the host and exfiltrate sensitive data from both the network and the local filesystem.
Affected Component
Entry point: src/index.ts registers the execute tool with a free-form code: string parameter and no authentication, authorization, or input sanitization.
Execution path: executeCode() in src/vm-executor.ts wraps the supplied string in an async IIFE and evaluates it via vm.Script.runInContext().
Root Cause Analysis
The vulnerability is architectural, not an implementation bug in the traditional sense. The server is designed to run arbitrary Puppeteer scripts on behalf of the MCP client. Security-relevant design decisions compound as follows.
1. Unrestricted code execution is the core feature
The execute tool accepts any JavaScript string and executes it server-side:
// src/index.ts
server.registerTool('execute', {
inputSchema: {
code: z.string().describe('JavaScript code to execute. Return JSON-serializable data'),
persistent: z.boolean().default(true),
},
}, async ({ code, persistent }) => {
const response = await executeCode(code, browser, timeout);
// ...
});
There is no allowlist of operations, no static analysis of the submitted code, and no per-call user identity to scope permissions. Any MCP client connected to the server can supply arbitrary code.
2. The Node.js VM sandbox is not a security boundary
User code is wrapped and executed inside vm.createContext():
// src/vm-executor.ts
export function wrapUserCode(code: string): string {
return `(async () => {\n${code}\n})()`;
}
function createContext(browser, logs, timers): vm.Context {
return vm.createContext({
browser,
console: createConsole(logs),
setTimeout,
clearTimeout,
URL,
URLSearchParams,
Buffer,
});
}
The project README states explicitly:
Not a sandbox: The Node.js VM isolates code for convenience, not security. It is not designed to run untrusted code.
Node.js documentation likewise warns that node:vm must not be treated as an isolation mechanism against untrusted code. The sandbox blocks direct access to require, process, __dirname, and __filename, but that isolation is trivially bypassed for practical attacks because the browser object is injected into the sandbox—a fully privileged Puppeteer handle to a real Chrome instance running on the host.
3. Full Puppeteer Browser access enables host-adjacent attacks
Passing the live browser global into the VM context gives executed code the complete Puppeteer API, including:
browser.newPage() / page.goto(url) — navigate to arbitrary URLs, including internal services and file:// paths.
page.content() / page.title() — read rendered page data.
page.screenshot({ path }) / page.pdf({ path }) — write files to arbitrary writable paths on the host filesystem.
browser.process() — obtain the Chrome child-process PID.
Because Chrome is launched with --no-sandbox and --disable-setuid-sandbox (see src/browser-manager.ts), the browser process itself runs with reduced OS-level isolation, which further increases blast radius in container and multi-user environments.
4. No MCP-layer access control
The server communicates over stdio with no authentication, session binding, or tool-level authorization. In a typical MCP deployment, whoever can configure or reach the MCP server can invoke all registered tools. Combined with LLM prompt injection, an attacker can craft instructions that cause the model to call execute with malicious payloads without the end user's awareness.
5. Persistent browser sessions amplify impact
With persistent: true (the default), cookies, local storage, and login sessions survive across tool invocations. An attacker who gains code execution can harvest credentials from an authenticated browser profile on subsequent calls.
Verification Environment
Prerequisites
Node.js ≥ 20
Project cloned and dependencies installed (npm install)
TypeScript build output available (npx tsc -p tsconfig.build.json)
Launch MCP Inspector
From the project root:
npx @modelcontextprotocol/[email protected] node dist/cli.js
Inspector prints a local URL with an authentication token, e.g.:
http://localhost:6274/?MCP_PROXY_AUTH_TOKEN=<token>
Open that URL in a browser, connect to the server, and navigate to Tools → execute.
For isolated testing, set persistent to false unless session-reuse behavior is explicitly under test.
Verified Payloads
PoC 1 — Arbitrary JavaScript Execution
Objective: Confirm that attacker-controlled JavaScript runs on the MCP server host.
Payload (code field):
return 1 + 1
Result: Confirmed. The server evaluates and returns the result of arbitrary expressions. This establishes the code-execution primitive.
PoC 2 — Unrestricted Browser Navigation and Page Content Exfiltration
Objective: Confirm that executed code can drive Chrome to any URL and read page content back through the MCP response.
Payload (code field):
const page = await browser.newPage();
await page.goto('https://example.com');
return {
title: await page.title(),
url: page.url()
};
Result: Confirmed. Executed code can:
Open arbitrary network destinations (public internet, internal RFC 1918 hosts, cloud metadata endpoints such as x.x.x.x, etc.).
Extract DOM-visible content and return it in the tool response JSON.
Trigger automatic per-tab JPEG screenshots (returned as local file paths in the screenshots array), providing visual exfiltration of every open tab.
This primitive enables SSRF-style attacks against services reachable from the host running pptr-mcp.
PoC 3 — Local Filesystem Read via file://
Objective: Confirm that the controlled browser can read local files on the host filesystem, bypassing the Node.js VM's restriction on direct fs access.
Payload (code field, Windows paths):
const page = await browser.newPage();
await page.goto('file:///C:/Windows/System32/drivers/etc/hosts');
return await page.content();
Note: Adjust the path for your OS. On Linux/macOS, e.g. file:///etc/passwd.
Result: Confirmed. The full contents of the local hosts file were returned in the MCP tool response, demonstrating that:
The VM sandbox does not prevent filesystem access when the browser escape hatch is available.
Any file readable by Chrome via file:// (subject to OS file permissions) can be exfiltrated.
Threat scenarios:
Prompt injection: A malicious webpage or document instructs the LLM to call execute with an exfiltration payload.
Malicious MCP client configuration: A trojaned MCP config points to pptr-mcp and invokes execute directly.
Shared / multi-tenant deployment: Running pptr-mcp as a shared service (explicitly discouraged by upstream) would expose all connected clients to full host-adjacent compromise.
Recommendations
For operators
Treat pptr-mcp as a privileged local tool. Only connect it from trusted MCP clients in single-user development environments.
Never expose the MCP server to untrusted networks or run it as a shared service.
Use persistent: false when isolation between invocations is required.
Avoid configuring the server while authenticated to sensitive web applications; the persistent profile retains session state.
For upstream maintainers (if hardening is desired)
Replace free-form code execution with a fixed set of parameterized, allowlisted operations (navigate, click, screenshot, etc.).
If programmatic flexibility must remain, run executed code in a dedicated OS-level sandbox (VM, container with seccomp/AppArmor, gVisor) rather than node:vm.
Launch Chrome with sandbox enabled where the deployment environment supports it; document the trade-off explicitly.
Add optional authentication or tool-level authorization at the MCP transport layer.
Disable or restrict file:// navigation via Chrome launch flags (e.g., --allow-file-access-from-files avoidance, custom URL blocklists).
Document the RCE classification prominently in the README and MCP registry listing so consumers perform informed risk acceptance.
|
|---|