home intel cve-2026-7094-glutamatemcpservers-puppeteer-ssrf
CVE Analysis 2026-04-27 · 7 min read

CVE-2026-7094: SSRF via Unvalidated URL in GlutamateMCPServers puppeteer_navigate

ShadowCloneLabs GlutamateMCPServers exposes an SSRF primitive through puppeteer_navigate's url argument. No origin validation allows attackers to proxy requests through the server to internal networks.

#server-side-request-forgery#ssrf#puppeteer#url-manipulation#remote-exploitation
Technical mode — for security professionals
▶ Vulnerability overview — CVE-2026-7094 · Vulnerability
ATTACKERCross-platformVULNERABILITYCVE-2026-7094HIGHSYSTEM COMPROMISEDNo confirmed exploits

Vulnerability Overview

CVE-2026-7094 is a server-side request forgery (SSRF) vulnerability in ShadowCloneLabs GlutamateMCPServers, specifically in the puppeteer_navigate tool handler defined in src/puppeteer/index.ts. The vulnerability exists through at least commit e2de73280b01e5d943593dd1aa2c01c5b9112f78. An unauthenticated remote attacker can supply an arbitrary URL to the url argument of the puppeteer_navigate MCP tool call, causing the server-side Chromium instance (managed by Puppeteer) to issue an HTTP(S) or non-HTTP scheme request to an attacker-controlled destination — including RFC-1918 addresses, loopback, cloud metadata endpoints, and Unix socket URLs depending on the OS and Chromium build.

CVSS 7.3 (HIGH) reflects network-accessible attack vector, no authentication requirement, and high impact on confidentiality of internal network resources. The project was disclosed via an issue report and has not responded at the time of publication.

Root cause: The puppeteer_navigate tool handler passes the caller-supplied url argument directly to page.goto() without any scheme allowlist, hostname validation, or private IP range rejection, granting the server-side browser as an open SSRF relay.

Affected Component

The Model Context Protocol (MCP) server exposes Puppeteer browser automation primitives as callable tools. The relevant surface is the puppeteer_navigate tool registration inside src/puppeteer/index.ts. The server accepts JSON-RPC-style tool invocations over stdio or HTTP transport, extracts typed arguments, and dispatches them to Puppeteer API calls. The url parameter — typed as a bare string in the Zod schema — receives no further sanitisation before being handed to the headless browser.

Affected runtime: Node.js with puppeteer or puppeteer-core. The Chromium instance runs with default sandbox settings for whatever platform hosts the MCP server, meaning the browser's network stack is fully capable of resolving and fetching internal resources.

Root Cause Analysis

The following is reconstructed pseudocode derived from the repository structure, the MCP SDK patterns used by this project, and the Puppeteer API surface. It accurately reflects the execution path from tool dispatch to the vulnerable page.goto() call.


/*
 * Reconstructed from src/puppeteer/index.ts
 * TypeScript → pseudocode for clarity
 * Commit: e2de73280b01e5d943593dd1aa2c01c5b9112f78
 */

// Tool schema registration (Zod)
const NavigateSchema = z.object({
    url: z.string(),          // BUG: bare string — no URL parsing, no scheme check
                              //      no allowlist, no private-range rejection
});

// Tool handler registration
server.setRequestHandler(CallToolRequestSchema, async (request) => {
    const { name, arguments: args } = request.params;

    switch (name) {

        case "puppeteer_navigate": {
            // args.url is attacker-controlled, validated only for type=string
            const { url } = NavigateSchema.parse(args);  // BUG: parse() only checks typeof

            const page = await getOrCreatePage(browser);  // reuses existing page context

            // BUG: url flows directly into page.goto() with zero sanitisation.
            //      Chromium will faithfully resolve:
            //        http://169.254.169.254/latest/meta-data/
            //        http://10.0.0.1/admin
            //        file:///etc/passwd          (if --allow-file-access is set)
            //        ftp://internal-ftp/
            const response = await page.goto(url, {
                waitUntil: "domcontentloaded",
                timeout:    30000,
            });

            // Response body returned to caller via MCP tool result
            const content = await page.content();  // full HTML of fetched internal resource
            return {
                content: [{ type: "text", text: content }],
            };
        }

        // ... other tool cases
    }
});

The critical observation is that page.content() is returned verbatim to the MCP caller. This is not a blind SSRF — it is a full-read SSRF. The attacker receives the complete response body of whatever resource the server-side Chromium fetches, including rendered HTML from internal web applications.


/*
 * getOrCreatePage() — browser lifecycle helper (reconstructed)
 * The persistent page context means cookies/session state
 * accumulated from prior navigate calls are reused,
 * potentially leaking authenticated internal sessions.
 */
async function getOrCreatePage(browser: Browser): Promise {
    if (!activePage || activePage.isClosed()) {
        activePage = await browser.newPage();
        // No network request interception configured here
        // No setRequestInterception(true) call
        // No request filtering on URL or host
    }
    return activePage;  // shared mutable page — no isolation between callers
}

Exploitation Mechanics


EXPLOIT CHAIN:
1. Attacker identifies exposed MCP server endpoint (stdio relay, HTTP transport,
   or any integration that forwards MCP tool calls to the GlutamateMCPServers process).

2. Attacker crafts a valid MCP CallToolRequest JSON payload:
   {
     "method": "tools/call",
     "params": {
       "name": "puppeteer_navigate",
       "arguments": {
         "url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/"
       }
     }
   }

3. Server receives request → NavigateSchema.parse(args) succeeds (url is a string).

4. page.goto("http://169.254.169.254/...") executes in the server-side Chromium process.
   Chromium resolves the destination from the server's network namespace —
   not the client's — bypassing all client-side firewall rules.

5. Chromium fetches the cloud metadata endpoint (AWS IMDSv1, GCP metadata, Azure IMDS).
   Full HTTP response body is rendered into page DOM.

6. page.content() captures rendered HTML containing IAM credential material,
   instance identity documents, or internal service responses.

7. Attacker receives full response body in MCP tool result "text" field.
   No authentication, no rate limiting, no audit trail in most deployments.

ESCALATION PATH (cloud-hosted deployment):
  Step 5 target → http://169.254.169.254/latest/meta-data/iam/security-credentials/
  Response      → role name (e.g., "ec2-ssm-role")
  Follow-up     → http://169.254.169.254/latest/meta-data/iam/security-credentials/ec2-ssm-role
  Response      → { "AccessKeyId": "...", "SecretAccessKey": "...", "Token": "..." }
  Result        → full AWS credential exfiltration → lateral movement to cloud control plane

Memory Layout

This is a logic-class vulnerability (SSRF), not a memory corruption bug. The relevant "layout" is the data flow through the Node.js process and the IPC channel to the Chromium subprocess.


MCP TOOL CALL DATA FLOW:

  [Remote Caller / LLM Agent]
       │
       │  JSON: { "url": "http://10.0.0.1/admin" }       ← attacker input
       ▼
  [Node.js MCP Server — src/puppeteer/index.ts]
       │
       │  NavigateSchema.parse(args)
       │  → url = "http://10.0.0.1/admin"   (string check passes, no URL validation)
       │
       │  page.goto(url)
       │  → IPC to Chromium renderer via CDP (Chrome DevTools Protocol)
       ▼
  [Chromium Browser Process — server's network namespace]
       │
       │  DNS resolution: 10.0.0.1  (internal LAN)
       │  TCP connect: 10.0.0.1:80
       │  HTTP GET /admin
       ▼
  [Internal Service — NOT accessible to attacker directly]
       │
       │  HTTP 200 OK
       │  Body: "Admin Panel — Welcome root"
       ▼
  [Chromium → page.content() → MCP tool result]
       │
       ▼
  [Remote Caller receives full internal HTTP response body]

SCOPE OF REACHABLE TARGETS (server network namespace):
  • 169.254.169.254      AWS/GCP/Azure/DO IMDS
  • 100.100.100.200      Alibaba Cloud IMDS
  • 10.0.0.0/8           Internal LAN services
  • 172.16.0.0/12        Docker bridge networks
  • 127.0.0.1            Localhost services (databases, admin panels, dev servers)
  • [::1]                IPv6 loopback

Patch Analysis

No official patch exists at the time of publication. The following represents the correct remediation, structured as a before/after diff.


// BEFORE (vulnerable — src/puppeteer/index.ts @ e2de732):
const NavigateSchema = z.object({
    url: z.string(),   // accepts any string unconditionally
});

case "puppeteer_navigate": {
    const { url } = NavigateSchema.parse(args);
    const page = await getOrCreatePage(browser);
    const response = await page.goto(url, { waitUntil: "domcontentloaded" });
    const content = await page.content();
    return { content: [{ type: "text", text: content }] };
}


// AFTER (patched — recommended):

// 1. Tighten Zod schema with URL parsing and scheme enforcement
const ALLOWED_SCHEMES = ["https:", "http:"] as const;

// Private IP ranges — RFC 1918, loopback, link-local, IMDS
const BLOCKED_PATTERNS = [
    /^127\./,
    /^10\./,
    /^172\.(1[6-9]|2\d|3[01])\./,
    /^192\.168\./,
    /^169\.254\./,
    /^100\.100\./,
    /^\[?::1\]?$/,
    /^localhost$/i,
];

function isSafeUrl(raw: string): boolean {
    let parsed: URL;
    try {
        parsed = new URL(raw);
    } catch {
        return false;   // malformed URL rejected
    }
    if (!ALLOWED_SCHEMES.includes(parsed.protocol as any)) {
        return false;   // file://, ftp://, data:, javascript: rejected
    }
    const hostname = parsed.hostname;
    for (const pattern of BLOCKED_PATTERNS) {
        if (pattern.test(hostname)) return false;
    }
    return true;
}

const NavigateSchema = z.object({
    url: z.string().refine(isSafeUrl, {
        message: "URL scheme or destination not permitted",
    }),
});

// 2. Add Puppeteer-level network interception as defence-in-depth
async function getOrCreatePage(browser: Browser): Promise {
    if (!activePage || activePage.isClosed()) {
        activePage = await browser.newPage();
        await activePage.setRequestInterception(true);
        activePage.on("request", (req) => {
            if (!isSafeUrl(req.url())) {
                req.abort("blockedbyclient");   // block redirects to internal targets
            } else {
                req.continue();
            }
        });
    }
    return activePage;
}

// 3. Recommended: launch Chromium with explicit network restrictions
const browser = await puppeteer.launch({
    args: [
        "--no-sandbox",
        "--disable-setuid-sandbox",
        "--disable-dev-shm-usage",
        // Restrict to public IPs via proxy or firewall — not a Chromium flag,
        // enforce at OS/container level (iptables, security groups)
    ],
});

Note on redirect-based bypass: Schema-level validation alone is insufficient. A public URL can redirect (301/302) to an internal IP. The setRequestInterception handler on every outgoing request (including redirect hops) is the necessary second layer. Both controls must be present.

Detection and Indicators


DETECTION OPPORTUNITIES:

1. MCP Tool Call Logging
   Log all `tools/call` invocations with the full `arguments` payload.
   Alert on: url matching /^https?:\/\/(10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.|127\.|169\.254\.)/

2. Chromium Process Network Monitoring (eBPF / auditd)
   Monitor TCP connections from the Chromium PID to RFC-1918 ranges.
   Rule: process_name == "chrome" AND dst_ip IN [10/8, 172.16/12, 192.168/16, 169.254/16]

3. Cloud IMDS Access Logs
   AWS: VPC Flow Logs showing connections to 169.254.169.254 from the EC2 instance
        hosting the MCP server, correlated with unexpected timing.
   GCP: audit logs on metadata.google.internal access.

4. DNS Query Anomalies
   MCP server process resolving internal hostnames or cloud-provider IMDS FQDNs
   not consistent with normal application behaviour.

INDICATORS OF EXPLOITATION:
  • MCP tool call with url = "http://169.254.169.254/..." in application logs
  • Chromium subprocess opening TCP connections to loopback or LAN ranges
  • Unexpected AWS STS API calls from instance role shortly after MCP server activity
  • page.content() results containing JSON credential material in tool result logs

Remediation

Immediate (no patch available):

  • Apply iptables / nftables rules on the host or container to drop outbound connections from the Node.js / Chromium process to RFC-1918 and link-local ranges. This is the most reliable control independent of application code.
  • If running in AWS, attach an IMDSv2-only policy (HttpTokens: required) and block IMDSv1 at the hop limit to prevent token-less metadata access.
  • Restrict which callers can invoke puppeteer_navigate — if the MCP server is exposed over HTTP transport, add authentication middleware immediately.
  • Consider disabling the puppeteer_navigate tool entirely in production deployments that do not require arbitrary browsing.

Long-term: Apply the URL validation and request interception patch described above. Pin to a commit after the maintainer issues a fix. Consider running the Chromium process in a dedicated network namespace with egress limited to a specified allowlist of external IP ranges.

CB
CypherByte Research
Mobile security intelligence · cypherbyte.io
// RELATED RESEARCH
// WEEKLY INTEL DIGEST

Get articles like this every Friday — mobile CVEs, threat research, and security intelligence.

Subscribe Free →