GHSA-mrvx-jmjw-vggc: SearXNG MCP Server: DNS-resolved Private Hostname SSRF in `web_url_read`
DNS-resolved Private Hostname SSRF in `web_url_read`SummaryThe `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.DetailsThe vulnerable code is in `src/url-reader.ts`, where the `assertUrlAllowed()` function performs a hostname-only string comparison: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:`src/index.ts:37-49` — `isWebUrlReadArgs()` accepts `args.url` as any string value with no URL policy enforcement.`src/index.ts:226-240` — `web_url_read` passes `args.url` directly to `fetchAndConvertToMarkdown()`.`src/url-reader.ts:307-313` — URL is parsed and `assertUrlAllowed(parsedUrl)` is called (string check only).`src/url-reader.ts:85-93` — `assertUrlAllowed()` checks `url.hostname` as a string; no DNS resolution is performed.`src/url-reader.ts:367` — `undiciFetch(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:PoC**Prerequisites:**Docker installed on the test machine.The `mcp-searxng` repository cloned locally (version 1.6.0 / commit `90423e5`).**Build the container:****Run the PoC:**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:**Starts a Python HTTP server on `127.0.0.1:9796` returning the sentinel value `INTERNAL-SECRET-OK-SSRF-PROOF`.Starts the MCP server in STDIO mode.**Test 1:** Sends a `tools/call` request for `http://127.0.0.1:9796/` — this is correctly **blocked** by `assertUrlAllowed()`.**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):**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):**ImpactThis 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``poc.py`
CVSS v3.1: 7.1