Skip to content

[Security] SSRF: No URL validation in navigation and HTTP API tools allows internal network access #230

Description

@Correctover

Summary

All URL-accepting tools in this MCP server (navigate, GET/POST/PUT/PATCH/DELETE) pass user-supplied URLs directly to Playwright without any validation. This allows SSRF attacks including access to cloud metadata endpoints, internal network services, and local files via file:// protocol.

Affected Code

Navigation Tool

File: src/tools/browser/navigation.ts, line ~31

await page.goto(args.url, {
  timeout: args.timeout || 30000,
  waitUntil: args.waitUntil || "load"
});
// No URL validation - accepts any URL including file://, internal IPs, etc.

API Request Tools (GET/POST/PUT/PATCH/DELETE)

File: src/tools/api/requests.ts, lines ~99, ~144, ~190, ~236, ~272

const response = await apiContext.get(args.url, { headers: ... });
// No URL validation on any of the 5 HTTP method tools

Attack Scenarios

1. Cloud Metadata Theft

playwright_navigate(url="http://169.254.169.254/latest/meta-data/iam/security-credentials/")
→ Returns AWS IAM credentials

2. Local File Read

playwright_navigate(url="file:///etc/passwd")
→ Returns contents of /etc/passwd

3. Internal Network Access

playwright_get(url="http://192.168.1.1:8080/admin")
→ Accesses internal admin panels

4. Internal Port Scanning

playwright_get(url="http://10.0.0.1:3306/") → MySQL
playwright_get(url="http://10.0.0.1:6379/") → Redis

Impact

  • CVSS 7.8 (AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:N)
  • Affects all deployments where an AI agent uses this MCP server
  • Prompt injection attacks can redirect the agent to call these tools with malicious URLs
  • Cloud deployments are especially at risk (metadata service attacks)

Suggested Fix

import { URL } from "url";

const BLOCKED_HOSTS = new Set([
  "169.254.169.254", "metadata.google.internal", "100.100.100.200",
  "localhost", "127.0.0.1", "::1", "0.0.0.0"
]);

function validateUrl(url: string): void {
  const parsed = new URL(url);
  
  // Block file:// protocol
  if (parsed.protocol === "file:") {
    throw new Error("file:// protocol is not allowed");
  }
  
  // Block internal IPs
  if (BLOCKED_HOSTS.has(parsed.hostname)) {
    throw new Error(`Access to ${parsed.hostname} is blocked`);
  }
  
  // Block private IP ranges
  if (/^(10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.)/.test(parsed.hostname)) {
    throw new Error("Private IP addresses are not allowed");
  }
}

Environment

  • mcp-playwright latest version
  • Node.js runtime

Reported as part of MCP ecosystem security research by Correctover (https://correctover.com).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions