Skip to main content
Mythos

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-label with 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_evaluate and browser_run_code provide 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_evaluate can run document.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_navigate to any non-localhost URL β€” hard deny via permissionDecision: "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 content
  • browser_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 tools
  • browser_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

  • --isolated flag 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.json lives in the plugin marketplace directory and could be overwritten on updates. Re-check after plugin syncs

Contexts

🏷️#claude-code 🏷️#mcp 🏷️#security 🏷️#playwright 🏷️#agentic 🏷️#infrastructure

Created with πŸ’œ by One Inc | Copyright 2026