KEVWire

Security intelligence, with context.

Response snapshot Oct 29, 2026, 02:31 UTC

Back to the wire
Permalink
VulnerabilityGHSA-4mfg-r38w-w8fg

mcp-sanitizer: URL sanitizer allows localhost/IPv6 loopback SSRF bypass via explicit port or bracketed IPv6

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

At a glance

Exploited
Not known
Severity
Not scored yet
Chance of exploitation
Not estimated
Sources
1 source

What to do

GitHub lists first patched versions: mcp-sanitizer: 1.4.0. Confirm the affected range and upgrade guidance in the exact advisory.

Read GitHub-reviewed advisory GHSA-4mfg-r38w-w8fg (opens a new tab)

What happened

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

Continue reading

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

Affected package ranges reported by GitHub: mcp-sanitizer: < 1.4.0

Related technology

Sources

  1. Primary source

    GitHub-reviewed advisory GHSA-4mfg-r38w-w8fg (opens a new tab)

    The public advisory supplies the description, affected package/version ranges, and any listed CVSS metric. GitHub review is not local review.

    Published Checked

  2. Primary source

    GitHub public advisory API page (opens a new tab)

    The API page supplied GHSA-4mfg-r38w-w8fg, its actual identifiers, source dates, and structured metrics.

    Published Date not suppliedChecked

Cite this record

For factual claims, cite the primary sources above; cite KEVWire for its compilation, dates, and classifications.

APA

KEVWire. (2026, October 28). mcp-sanitizer: URL sanitizer allows localhost/IPv6 loopback SSRF bypass via explicit port or bracketed IPv6. https://kevwire.com/record/GHSA-4mfg-r38w-w8fg

MLA

"mcp-sanitizer: URL sanitizer allows localhost/IPv6 loopback SSRF bypass via explicit port or bracketed IPv6." KEVWire, 28 Oct. 2026, kevwire.com/record/GHSA-4mfg-r38w-w8fg.

Chicago

KEVWire. "mcp-sanitizer: URL sanitizer allows localhost/IPv6 loopback SSRF bypass via explicit port or bracketed IPv6." Last modified October 28, 2026. https://kevwire.com/record/GHSA-4mfg-r38w-w8fg.

Evidence summary (plain text)

mcp-sanitizer: URL sanitizer allows localhost/IPv6 loopback SSRF bypass via explicit port or bracketed IPv6 (Oct 28, 2026).
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.
Sources:
GitHub-reviewed advisory GHSA-4mfg-r38w-w8fg: https://github.com/advisories/GHSA-4mfg-r38w-w8fg
GitHub public advisory API page: https://api.github.com/advisories?type=reviewed&per_page=100&sort=updated&direction=desc
Record ID: GHSA-4mfg-r38w-w8fg.
Record origin: Ingested record.
AI evidence: Not assessed.
Known exploitation: Not assessed.
Zero-day status: Not assessed. KEV membership alone does not establish zero-day status.
About this data

This record was ingested from a data source. Retrieved: Oct 29, 2026, 02:00 UTC. Ingestion is not an independent editorial investigation.

A CISA KEV listing documents known exploitation, not necessarily a zero-day; absence from the catalog does not prove a vulnerability has never been exploited.

Technology tags indicate possible relevance, not verified exposure. Confirm the affected product, version, and configuration against your own inventory; KEVWire does not scan your systems.

A personal reading checklist on this device, not verified remediation.

Record JSON

Your account

Email alerts

Email alerts are coming as part of KEVWire Premium: join the waitlist. To follow your stack today, use its RSS feed, which honors your filters, or connect an AI agent through the API.

Suggest an improvement

Ideas, missing sources, corrections, and bugs are all welcome. No account needed. Suggestions are reviewed by a person before anything changes.

Record details

Record detail