CVE-2026-49857: Verified Reproduction
CVE-2026-49857: auth-fetch-mcp SSRF via IPv4-mapped IPv6 loopback bypass
CVE-2026-49857 is verified against ymw0407/auth-fetch-mcp · npm. Affected versions: <= 3.0.1. Fixed in 3.0.2. Vulnerability class: SSRF. This high reproduction includes runnable sandbox proof, artifacts, and a plain-text agent view under REPRO-2026-00205.
What Is CVE-2026-49857?
CVE-2026-49857 is a high-severity SSRF (CWE-918) in the auth-fetch-mcp MCP server's URL security guard, allowing IPv4-mapped IPv6 loopback addresses to bypass its private-IP protection. Pruva reproduced it (reproduction REPRO-2026-00205).
CVE-2026-49857 Severity
CVE-2026-49857 is rated high severity.
High — serious impact or readily exploitable. Prioritize remediation.
Affected ymw0407/auth-fetch-mcp Versions
ymw0407/auth-fetch-mcp · npm versions <= 3.0.1 are affected.
How to Reproduce CVE-2026-49857
pruva-verify REPRO-2026-00205 curl -O https://www.pruva.dev/api/v1/reproductions/REPRO-2026-00205/artifacts/bundle/repro/reproduction_steps.sh && chmod +x reproduction_steps.sh && ./reproduction_steps.sh Proof of Reproduction for CVE-2026-49857
- reached the target end-to-end
- full exploit chain demonstrated
- on the real production code path
- high confidence
- the upstream fix blocks the same trigger
URL http://[::ffff:127.0.0.1]:18080/ passed to download_media tool via MCP JSON-RPC tools/call
- MCP tools/call download_media
- assertSafeUrl()
- isPrivateV6() hex-normalization bypass
- ctx.request.get() fetches loopback 127.0.0.1:18080
reproduction_steps.sh How the agent worked
Root Cause and Exploit Chain for CVE-2026-49857
The auth-fetch-mcp MCP server (npm package auth-fetch-mcp, versions ≤3.0.1) contains a Server-Side Request Forgery (SSRF) vulnerability in its URL security guard. The assertSafeUrl() function in src/security.ts is designed to block requests to private/loopback IP addresses, but its isPrivateV6() helper fails to detect IPv4-mapped IPv6 addresses after the Node.js WHATWG URL parser hex-normalizes them. When a user supplies a URL like http://[::ffff:127.0.0.1]:PORT/, the URL parser normalizes the hostname to ::ffff:7f00:1 (hex form). The guard checks net.isIPv4("7f00:1"), which returns false because the suffix is in hex notation, not dotted-decimal. As a result, the loopback address is classified as non-private and the security guard is bypassed, allowing the MCP server to fetch arbitrary internal/loopback URLs via the download_media and auth_fetch tools.
- Package/component affected:
auth-fetch-mcpnpm package, specificallysrc/security.ts(assertSafeUrl→isPrivateV6→isPrivateV4guard chain) - Affected versions: ≤3.0.1 (fixed in 3.0.2)
- Risk level: High
- Consequences: An attacker (via a malicious MCP client or prompt injection) can cause the MCP server to fetch arbitrary internal/loopback URLs, bypassing the SSRF protection. The
download_mediatool downloads the fetched content to disk and returns the file path to the caller, enabling information disclosure from internal services (e.g., cloud metadata endpoints at169.254.169.254, internal APIs on127.0.0.1/10.x/192.168.x, etc.). Theauth_fetchtool renders fetched internal pages in a browser and returns extracted content.
Impact Parity
- Disclosed/claimed maximum impact: SSRF — bypass of the private-IP guard to fetch internal/loopback URLs via the MCP server's
auth_fetchordownload_mediatools. - Reproduced impact from this run: Full SSRF confirmed. The MCP server (v3.0.1) processed a
download_mediatool call with URLhttp://[::ffff:127.0.0.1]:18080/, bypassedassertSafeUrl(), fetched content from a loopback HTTP server via Playwright'sctx.request.get(), saved the response to disk, and returned the file path. The downloaded file contained the secret marker from the internal server, proving the server-side request reached the loopback target. - Parity:
full - Not demonstrated: The reproduction targeted a loopback HTTP server (
127.0.0.1:18080). Cloud metadata endpoint (169.254.169.254) and other private ranges were not exercised at runtime, but the same bypass mechanism applies to all private IPv4 ranges via their IPv4-mapped IPv6 hex forms.
Root Cause
The vulnerability is in the isPrivateV6() function in src/security.ts:
function isPrivateV6(ip: string): boolean {
const lower = ip.toLowerCase();
if (lower === "::" || lower === "::1") return true;
if (lower.startsWith("fe80:") || lower.startsWith("fe80::")) return true;
if (lower.startsWith("fc") || lower.startsWith("fd")) return true;
if (lower.startsWith("ff")) return true;
if (lower.startsWith("::ffff:")) {
const v4 = lower.slice(7);
if (net.isIPv4(v4)) return isPrivateV4(v4); // ← BUG: only handles dotted-decimal
}
return false;
}
The bug: When the WHATWG URL parser processes http://[::ffff:127.0.0.1]:PORT/, it hex-normalizes the IPv4-mapped IPv6 hostname to ::ffff:7f00:1 (where 7f = 127, 00 = 0, 01 = 1). The isPrivateV6() function extracts the suffix 7f00:1 and checks net.isIPv4("7f00:1"), which returns false because the suffix is in hex notation, not dotted-decimal form. The function then falls through to return false, classifying the loopback address as non-private.
The call chain:
- MCP client sends
tools/callwithdownload_mediaand URLhttp://[::ffff:127.0.0.1]:PORT/ download_mediahandler callsassertSafeUrl(url)insrc/tools.ts:233assertSafeUrl()callsnew URL(rawUrl)— the WHATWG parser normalizes::ffff:127.0.0.1→::ffff:7f00:1assertSafeUrl()extractshostname=::ffff:7f00:1, callsisPrivateOrLinkLocal("::ffff:7f00:1")isPrivateOrLinkLocal()callsisPrivateV6("::ffff:7f00:1")(sincenet.isIPv6()returnstrue)isPrivateV6()checks::ffff:prefix, extracts7f00:1,net.isIPv4("7f00:1")=false→ returnsfalse(bypass!)assertSafeUrl()returns the parsed URL — security check passedctx.request.get(safeUrl.toString())fetcheshttp://[::ffff:7f00:1]:PORT/→ OS maps to127.0.0.1:PORT→ SSRF succeeds
Fix commit: 177ec5f8ee9c2d5749035777e562f699971b0da9 — adds hex group parsing to reconstruct the IPv4 address from the two trailing hex groups and run it through isPrivateV4():
const groups = v4.split(":");
if (groups.length === 2 && groups.every((g) => /^[0-9a-f]{1,4}$/.test(g))) {
const hi = parseInt(groups[0], 16);
const lo = parseInt(groups[1], 16);
const mapped = `${(hi >> 8) & 0xff}.${hi & 0xff}.${(lo >> 8) & 0xff}.${lo & 0xff}`;
return isPrivateV4(mapped);
}
Reproduction Steps
- Reference:
bundle/repro/reproduction_steps.sh(self-contained, usesbundle/repro/mcp_client.js) - What the script does:
- Reads
bundle/project_cache_context.jsonto locate the prepared project cache and Playwright browser cache - Clones/reuses the
auth-fetch-mcprepo at the project cache path - Installs Chrome Headless Shell for Playwright (manual download from Google's Chrome for Testing CDN, since
npx playwright installdoesn't support Ubuntu 26.04) - Installs system libraries required by Chrome
- Builds vulnerable version v3.0.1 (commit
98f381d) - Starts a local "victim" HTTP server on
127.0.0.1:18080that returns a unique secret marker - Spawns the real MCP server (
node dist/index.js) as a child process - Sends JSON-RPC
initialize+tools/call download_mediawith URLhttp://[::ffff:127.0.0.1]:18080/over stdio - Checks if the downloaded file contains the secret marker (SSRF confirmed) or if the response contains "Refusing to fetch" (blocked)
- Repeats for fixed version v3.0.2 (commit
d4dedaf, fix177ec5f) - Writes
runtime_manifest.jsonwith evidence
- Reads
- Expected evidence:
- Vulnerable v3.0.1: victim server receives HTTP request from
127.0.0.1, downloaded file contains the secret marker, tool returnsdownloaded: 1 - Fixed v3.0.2: victim server receives no request, tool returns
error: "Refusing to fetch [::ffff:7f00:1]...",downloaded: 0
- Vulnerable v3.0.1: victim server receives HTTP request from
Evidence
Log files (under
bundle/logs/):reproduction_steps.log— full script outputvulnerable_test.log— vulnerable version test outputvulnerable_result.json— structured result:ssrfConfirmed: true,downloadedContent: "SSRF_SECRET_MARKER_..."vulnerable_victim_server.log— showsRequest from 127.0.0.1 path=/(SSRF request received)vulnerable_mcp_stdout.log— MCP server JSON-RPC responsesvulnerable_mcp_requests.log— JSON-RPC requests sentfixed_test.log— fixed version test outputfixed_result.json— structured result:blocked: true, error:"Refusing to fetch [::ffff:7f00:1]..."fixed_victim_server.log— shows NO request received (only "Listening" line)fixed_mcp_stdout.log— MCP server JSON-RPC responses showing the block
Key excerpts:
- Vulnerable victim server log:
[VICTIM:18080] Request from 127.0.0.1 path=/ - Vulnerable downloaded file:
SSRF_SECRET_MARKER_1783015602673_hgnjlkyh(matches marker from internal server) - Vulnerable tool result:
{"status":"ok","downloaded":1,"total":1,"files":[{"url":"http://[::ffff:127.0.0.1]:18080/","localPath":".../file-1.bin","size":41}]} - Fixed tool result:
{"status":"ok","downloaded":0,"total":1,"files":[{"url":"http://[::ffff:127.0.0.1]:18080/","error":"Refusing to fetch [::ffff:7f00:1] (resolves to private/loopback/link-local address ::ffff:7f00:1)..."}]}
- Vulnerable victim server log:
Environment:
- Node.js v24.18.0, npm 11.16.0
- Ubuntu 26.04 LTS (Resolute Raccoon)
- Playwright 1.58.2 with Chrome Headless Shell 145.0.7632.6 (manually installed)
- MCP SDK
@modelcontextprotocol/sdk^1.27.1 (from package-lock.json)
Recommendations / Next Steps
- Upgrade: Update to
auth-fetch-mcp@3.0.2or later, which includes the fix (commit177ec5f). - Suggested fix approach (already implemented in 3.0.2): Parse the two trailing hex groups of
::ffff:prefixed IPv6 addresses back into dotted-decimal IPv4 form and run throughisPrivateV4(). Additionally, consider using a well-maintained SSRF protection library (e.g.,ssrf-checkor equivalent) rather than custom IP range checks. - Testing recommendations: Add unit tests for
assertSafeUrl()covering:- IPv4-mapped IPv6 in both dotted-decimal (
::ffff:127.0.0.1) and hex (::ffff:7f00:1) forms - All private ranges:
127.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,169.254.0.0/16(link-local/metadata) - Public IPv4-mapped addresses should still pass (e.g.,
::ffff:8.8.8.8) - Integration tests that verify the MCP tool rejects loopback URLs end-to-end
- IPv4-mapped IPv6 in both dotted-decimal (
Additional Notes
- Idempotency: The script was run twice consecutively with identical results (both runs: vulnerable=SSRF confirmed, fixed=blocked). The script cleans up previous test state (removes old HOME directories, overwrites result files) and uses unique markers per run.
- Chrome installation workaround: Playwright 1.58.2 does not support
npx playwright installon Ubuntu 26.04. The script manually downloads Chrome Headless Shell and Chrome for Testing from Google's CDN (storage.googleapis.com/chrome-for-testing-public/) and places them in the Playwright browser cache directory. ThePLAYWRIGHT_BROWSERS_PATHenvironment variable is set so Playwright finds the manually installed browser regardless ofHOME. - MCP transport: The auth-fetch-mcp server uses stdio JSON-RPC (StdioServerTransport), not HTTP. The reproduction interacts with it via its real JSON-RPC API by spawning the server process and sending protocol messages over stdin/stdout. This is the actual API surface of the product — the tool-calling endpoint that processes attacker-supplied URLs.
- Port choice: The victim server uses port 18080 to avoid conflicts with common services.
CVE-2026-49857 Reproduction Transcript
The agent's step-by-step process — every tool call, every handoff, the moment the exploit fired.
Full session Replay every step — scrub the timeline or play it back.
ls -la /data/pruva/project-cache/c1700ca0-202a-4b72-bf90-7542807cb1eb/ 2>/dev/null && echo "---REPO---" && ls -la /data/pruva/project-cache/c1700ca0-202a-4b72-bf90-7542807cb1eb/repo 2>/dev/null && echo "---MIRRORS---" && ls -la /data/pruva/project-cache/c1700ca0-202a-4b72-bf90-7542807cb1eb/repo-mirrors 2>/dev/nulltotal 0 drwxr-xr-x 1 vscode vscode 24 Jul 2 17:59 . drwxr-xr-x 1 vscode vscode 1750 Jul 2 17:59 .. drwxr-xr-x 1 vscode vscode 0 Jul 2 17:59 repo-mirrors ---REPO---
node --version 2>/dev/null; npm --version 2>/dev/null; which node; which npm; echo "---"; git --versionv24.18.0 11.16.0 /usr/bin/node /usr/bin/npm --- git version 2.55.0
Artifacts and Evidence for CVE-2026-49857
Scripts, logs, diffs, and output captured during the reproduction.
How to Fix CVE-2026-49857
Upgrade ymw0407/auth-fetch-mcp · npm to 3.0.2 or later.
FAQ: CVE-2026-49857
How does the CVE-2026-49857 SSRF bypass work in practice?
Which auth-fetch-mcp versions are affected by CVE-2026-49857, and where is it fixed?
How severe is CVE-2026-49857?
How can I reproduce CVE-2026-49857?
References for CVE-2026-49857
Authoritative sources for CVE-2026-49857 — official vulnerability databases and the upstream advisory. Pruva's reproduction verifies the issue firsthand; these are the primary records to corroborate it.