The Problem
Microsoft's π·οΈ#playwright π·οΈ#mcp server (@playwright/mcp) gives πClaude Code full browser automation β navigation, screenshots, JS execution, cookie access. The maintainers themselves state: "Playwright MCP is not a security boundary."
Attack Chain
- Claude navigates to a webpage
- Page contains hidden prompt injection in the accessibility tree (e.g., invisible
aria-labelwith instructions) - Claude follows injected instructions using its own tools β JS execution to extract cookies, navigation to exfiltrate data, or system command execution via sandbox escape
Specific Risks
- Prompt injection via accessibility snapshots β any webpage can embed hidden instructions that Claude can't distinguish from real prompts (issue 1479)
- Arbitrary JS execution β
browser_evaluateandbrowser_run_codeprovide full code execution. Confirmed RCE via prototype chain escape (issue 1495) - SSRF β unrestricted navigation can hit cloud metadata endpoints, internal services
- Cookie/token exposure β even without storage capabilities,
browser_evaluatecan rundocument.cookie - CVE-2025-9611 β DNS rebinding vulnerability (fixed in v0.0.40)
The Solution
A Claude Code PreToolUse hook that enforces localhost-only containment at the harness level β Playwright never sees a blocked request.
What's Blocked
browser_navigateto any non-localhost URL β hard deny viapermissionDecision: "deny"- Allows:
localhost,127.0.0.1,0.0.0.0,[::1], subdomains of localhost (e.g.,app.localhost:3000) - Blocks: everything else, including userinfo tricks (
[email protected]), hex IPs (0x7f000001), and protocol-relative URLs browser_evaluateβ blocked entirely, regardless of contentbrowser_run_codeβ blocked entirely, regardless of content
Why Block JS Execution Entirely?
Regex-based filtering of JavaScript is π·οΈ#security theater. String concatenation (window["fe"+"tch"]), eval(), Function() constructor, bracket notation, navigator.sendBeacon, new Image().src β there are dozens of bypass vectors. The only defensible posture is full block.
What's Allowed
browser_snapshotβ accessibility tree text (the primary design tool)browser_screenshotβ visual verification (the core use case)browser_click,browser_type,browser_hoverβ interaction toolsbrowser_network_requestsβ allowed with warning about exposed headers
The allowed tools are exactly what's needed for design verification β see πPlaywright for Frontend Design Verification for the full workflow.
Additional Hardening
--isolatedflag on the MCP server config (ephemeral sessions, no persistent cookies/storage)- No
--caps=storage(cookie/localStorage tools not exposed)
Setup Instructions
1. Hook Script
Place at ~/.claude/hooks/playwright-localhost-guard.py β a Python script that receives tool call JSON on stdin, parses the URL with urllib.parse.urlparse, and outputs permissionDecision: "deny" for violations.
2. Settings.json
Add to hooks.PreToolUse:
{
"matcher": "mcp__playwright",
"hooks": [{
"type": "command",
"command": "python3 ~/.claude/hooks/playwright-localhost-guard.py",
"timeout": 5000
}]
}3. MCP Config
Add --isolated to Playwright MCP args:
{"playwright": {"command": "npx", "args": ["@playwright/mcp@latest", "--isolated"]}}Known Limitations
- Redirect chains β if a localhost app returns a 302 to an external URL, Playwright follows it silently. The hook only checks the initial navigate target
- DNS rebinding β an attacker-controlled domain could resolve to 127.0.0.1. The hook checks the URL string, not the resolved IP
- Plugin updates β the
.mcp.jsonlives in the plugin marketplace directory and could be overwritten on updates. Re-check after plugin syncs
Contexts
π·οΈ#claude-code π·οΈ#mcp π·οΈ#security π·οΈ#playwright π·οΈ#agentic π·οΈ#infrastructure
