Skip to content

CVE-2026-49857: Verified Repro With Script Download

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.

REPRO-2026-00205 ymw0407/auth-fetch-mcp · npm SSRF Variant found Jul 2, 2026 CVE entry ↗ .txt
Severity
HIGH
Confidence
HIGH
Reproduced in
17m 11s
Tool calls
166
Spend
$2.60
01 · Overview

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

02 · Severity & CVSS

CVE-2026-49857 Severity

CVE-2026-49857 is rated high severity.

HIGH threat level
Weakness CWE-918 (SSRF) — Server-Side Request Forgery (SSRF)

High — serious impact or readily exploitable. Prioritize remediation.

03 · Affected Versions

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
or curl -O https://pruva.dev/api/v1/reproductions/REPRO-2026-00205/artifacts/bundle/repro/reproduction_steps.sh && chmod +x reproduction_steps.sh && ./reproduction_steps.sh
Run in a VM or disposable container. This exploits a real vulnerability.
06 · Proof of Reproduction

Proof of Reproduction for CVE-2026-49857

Authorization bypass — reproduced
  • 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
Trigger

URL http://[::ffff:127.0.0.1]:18080/ passed to download_media tool via MCP JSON-RPC tools/call

Attack chain
  1. MCP tools/call download_media
  2. assertSafeUrl()
  3. isPrivateV6() hex-normalization bypass
  4. ctx.request.get() fetches loopback 127.0.0.1:18080
Runnable proof: reproduction_steps.sh
Captured evidence: vulnerable testvulnerable victim servervulnerable mcp requestsvulnerable mcp stdoutfixed testfixed victim serverfixed mcp requestsfixed mcp stdout
Variants tested

Redirect-following SSRF bypass of the CVE-2026-49857 fix in auth-fetch-mcp. assertSafeUrl() is applied only to the initial request URL; Playwright ctx.request.get() (download_media, src/tools.ts:234) and page.goto() (auth_fetch via src/browser.ts:66) follow HTTP 3xx redirects to private/loopback IPs WITHOUT re-validat…

How the agent worked 395 events · 166 tool calls · 17 min
17 minDuration
166Tool calls
105Reasoning steps
395Events
Agent activity over 17 min
Support
17
Hypothesis
2
Repro
175
Judge
25
Variant
172
0:0017:11

Root Cause and Exploit Chain for CVE-2026-49857

Versions: ≤3.0.1 (fixed in 3.0.2)

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-mcp npm package, specifically src/security.ts (assertSafeUrlisPrivateV6isPrivateV4 guard 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_media tool 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 at 169.254.169.254, internal APIs on 127.0.0.1/10.x/192.168.x, etc.). The auth_fetch tool 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_fetch or download_media tools.
  • Reproduced impact from this run: Full SSRF confirmed. The MCP server (v3.0.1) processed a download_media tool call with URL http://[::ffff:127.0.0.1]:18080/, bypassed assertSafeUrl(), fetched content from a loopback HTTP server via Playwright's ctx.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:

  1. MCP client sends tools/call with download_media and URL http://[::ffff:127.0.0.1]:PORT/
  2. download_media handler calls assertSafeUrl(url) in src/tools.ts:233
  3. assertSafeUrl() calls new URL(rawUrl) — the WHATWG parser normalizes ::ffff:127.0.0.1::ffff:7f00:1
  4. assertSafeUrl() extracts hostname = ::ffff:7f00:1, calls isPrivateOrLinkLocal("::ffff:7f00:1")
  5. isPrivateOrLinkLocal() calls isPrivateV6("::ffff:7f00:1") (since net.isIPv6() returns true)
  6. isPrivateV6() checks ::ffff: prefix, extracts 7f00:1, net.isIPv4("7f00:1") = falsereturns false (bypass!)
  7. assertSafeUrl() returns the parsed URL — security check passed
  8. ctx.request.get(safeUrl.toString()) fetches http://[::ffff:7f00:1]:PORT/ → OS maps to 127.0.0.1:PORTSSRF 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

  1. Reference: bundle/repro/reproduction_steps.sh (self-contained, uses bundle/repro/mcp_client.js)
  2. What the script does:
    • Reads bundle/project_cache_context.json to locate the prepared project cache and Playwright browser cache
    • Clones/reuses the auth-fetch-mcp repo at the project cache path
    • Installs Chrome Headless Shell for Playwright (manual download from Google's Chrome for Testing CDN, since npx playwright install doesn'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:18080 that 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_media with URL http://[::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, fix 177ec5f)
    • Writes runtime_manifest.json with evidence
  3. Expected evidence:
    • Vulnerable v3.0.1: victim server receives HTTP request from 127.0.0.1, downloaded file contains the secret marker, tool returns downloaded: 1
    • Fixed v3.0.2: victim server receives no request, tool returns error: "Refusing to fetch [::ffff:7f00:1]...", downloaded: 0

Evidence

  • Log files (under bundle/logs/):

    • reproduction_steps.log — full script output
    • vulnerable_test.log — vulnerable version test output
    • vulnerable_result.json — structured result: ssrfConfirmed: true, downloadedContent: "SSRF_SECRET_MARKER_..."
    • vulnerable_victim_server.log — shows Request from 127.0.0.1 path=/ (SSRF request received)
    • vulnerable_mcp_stdout.log — MCP server JSON-RPC responses
    • vulnerable_mcp_requests.log — JSON-RPC requests sent
    • fixed_test.log — fixed version test output
    • fixed_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)..."}]}
  • 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.2 or later, which includes the fix (commit 177ec5f).
  • 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 through isPrivateV4(). Additionally, consider using a well-maintained SSRF protection library (e.g., ssrf-check or 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

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 install on 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. The PLAYWRIGHT_BROWSERS_PATH environment variable is set so Playwright finds the manually installed browser regardless of HOME.
  • 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.

Variant Analysis & Alternative Triggers for CVE-2026-49857

Versions: versions (as tested): v3.0.1 (98f381d, vulnerable), v3.0.2 (d4dedaf, fixed), and origin/main (a4b9245, latest) — bypass reproduces on all three.

A distinct, working Server-Side Request Forgery (SSRF) bypass of the CVE-2026-49857 fix exists in auth-fetch-mcp and reproduces end-to-end on the fixed v3.0.2 server and on the latest default branch (origin/main, a4b9245). The fix (commit 177ec5f) only hardened isPrivateV6() against the IPv4-mapped IPv6 hex-normalization vector. It did not change the fact that assertSafeUrl() is applied only to the initial request URL. Playwright's ctx.request.get() (used by download_media at src/tools.ts:234) and page.goto() (used by auth_fetch via src/browser.ts:66) follow HTTP 3xx redirects to private/loopback IPs without re-running assertSafeUrl() on the redirect target. An attacker supplies a public URL that 302-redirects to a private URL (e.g. http://httpbin.org/redirect-to?url=http://127.0.0.1:PORT/); the guard passes the public host, the redirect is followed to the loopback target with no re-validation, and the internal response is returned to the caller. This is a true bypass: the same SSRF guard, same tools, same sink, same trust boundary — a gap the fix does not cover.

Fix Coverage / Assumptions

  • Invariant the fix relies on: the URL that passes assertSafeUrl() is the URL that gets fetched. The fix only improves the IP classification inside isPrivateV6(); it never touches the fetch layer or redirect handling.
  • Code paths explicitly covered: src/security.ts isPrivateV6() for ::ffff:-prefixed IPv4-mapped addresses that the WHATWG parser normalizes to exactly two trailing hex groups (::ffff:7f00:1).
  • What the fix does NOT cover:
    1. Redirect targetsassertSafeUrl() is not invoked on Location targets of 3xx responses. (confirmed bypass)
    2. Non-mapped IPv4-in-IPv6 forms (::ffff:0:a.b.c.d, ::a.b.c.d, 64:ff9b::a.b.c.d) bypass the guard's string check, but the OS does not route them to IPv4 loopback in this environment, so no working SSRF (guard incompleteness only).
    3. DNS rebinding / TOCTOU between the guard's dns.lookup and the fetch's resolution (separate root cause).

Variant / Alternate Trigger

  • Entry point: the download_media MCP tool (and equivalently auth_fetch) over the real stdio JSON-RPC API — the product's actual tool-calling surface. A malicious MCP client or prompt-injected instruction calls tools/call download_media with:

    urls: [ "http://httpbin.org/redirect-to?url=http%3A%2F%2F127.0.0.1%3A18080%2F&status_code=302" ]
    
  • Why it bypasses the fix: httpbin.org resolves to public IPs only, so assertSafeUrl() (src/security.ts:87) passes it. The fixed isPrivateV6() is never even reached for a private address at guard time. httpbin.org responds 302 Location: http://127.0.0.1:18080/. ctx.request.get(safeUrl.toString()) at src/tools.ts:234 follows the redirect to the loopback target and downloads its body. assertSafeUrl() is not called again. The downloaded file is written to disk and its path/content returned to the caller.

  • Code path: tools.ts:233 assertSafeUrl(url) → passes (public) → tools.ts:234 ctx.request.get(safeUrl) → 302 → ctx.request.get follows → reaches 127.0.0.1:18080 (loopback victim) → tools.ts writes file-1.bin → returns {downloaded:1, files:[{localPath, size}]}. The sibling control URL http://127.0.0.1:18080/direct-control in the same call is correctly rejected with "Refusing to fetch 127.0.0.1 ...", proving the guard works for direct private URLs but not for redirect targets.

  • Package/component: auth-fetch-mcp npm package; SSRF guard in src/security.ts + fetch layer in src/tools.ts (download_media) and src/browser.ts (auth_fetch/navigateTo).

  • Affected versions (as tested): v3.0.1 (98f381d, vulnerable), v3.0.2 (d4dedaf, fixed), and origin/main (a4b9245, latest) — bypass reproduces on all three.

  • Risk level: High. An attacker can cause the MCP server to fetch arbitrary internal/loopback URLs (e.g. cloud metadata 169.254.169.254, internal APIs on 127.0.0.1/10.x/192.168.x) by pointing a public URL at them via redirect, then exfiltrate the response. download_media returns the fetched bytes to the caller (file path + content); auth_fetch renders and returns extracted page content.

Impact Parity

  • Disclosed/claimed maximum impact (parent CVE): SSRF — bypass of the private-IP guard to fetch internal/loopback URLs via auth_fetch/download_media.
  • Reproduced impact from this variant run: Full SSRF confirmed on the fixed server. The v3.0.2 MCP server processed download_media with the public 302-redirect URL, followed the redirect to http://127.0.0.1:18080/, the internal victim server received the request (Request from 127.0.0.1 path=/ host=127.0.0.1:18080), the response body (a unique secret marker) was downloaded to disk and returned to the caller (downloaded: 1, variant_redirect.ssrf: true, downloadedContent: "VARIANT_SSRF_MARKER_...").
  • Parity: full — same impact class (SSRF to loopback via the same guard/tools/sink) as the parent, achieved on the patched code.
  • Not demonstrated: Cloud metadata endpoint (169.254.169.254) and other private ranges were not exercised at runtime (no such endpoint in the sandbox), but the identical mechanism applies — any private IP reachable from the server can be the redirect target.

Root Cause

assertSafeUrl() (src/security.ts:87) is the product's SSRF gate, but it is invoked once on the caller-supplied URL (tools.ts:233, browser.ts:58). The downstream fetch (ctx.request.get / page.goto) follows redirects without re-invoking the gate. The CVE-2026-49857 fix (177ec5f) narrowed its change to the IP-classification helper isPrivateV6() and did not address redirect-following. Consequently, on the fixed code, any public URL that 3xx-redirects to a private/loopback URL produces SSRF: the guard sees only the public host, and the redirect target is fetched unvalidated. This is a gap in the same SSRF protection the fix belongs to (same guard, same tools, same sink, same trust boundary), reached via a different mechanism (redirect-following) than the original (IPv4-mapped IPv6 hex normalization). Fix commit: 177ec5f8ee9c2d5749035777e562f699971b0da9.

Reproduction Steps

  1. Reference: bundle/vuln_variant/reproduction_steps.sh (self-contained; uses bundle/vuln_variant/variant_mcp_client.js).
  2. What the script does:
    • Locates the prepared project cache (bundle/project_cache_context.json) for the repo + Playwright browser cache.
    • Creates git worktrees for v3.0.1 (vulnerable), v3.0.2 (fixed), and origin/main (latest) under bundle/artifacts/wt-*, reusing the cache's node_modules via symlink, and builds each (npm run build).
    • For each version, starts a loopback "victim" HTTP server on 127.0.0.1:18080 returning a unique secret marker, spawns the real MCP server (node dist/index.js), and sends JSON-RPC initialize + tools/call download_media with two URLs: a control http://127.0.0.1:18080/direct-control (must be blocked) and the variant http://httpbin.org/redirect-to?url=http://127.0.0.1:18080/&status_code=302 (public → 302 → loopback).
    • Verifies per version: control blocked by the guard, variant reached the loopback victim and downloaded the marker.
    • Writes bundle/vuln_variant/runtime_manifest.json and per-version *_variant_result.json under bundle/logs/vuln_variant/.
  3. Expected evidence:
    • Fixed v3.0.2: control_direct_loopback.blocked = true AND variant_redirect.ssrf = true (marker present in downloaded file) AND victim log shows Request from 127.0.0.1 path=/.
    • Latest main: same. Vulnerable v3.0.1: same.
    • Script exits 0 (bypass reproduced on the fixed version).

Evidence

  • Logs under bundle/logs/vuln_variant/:
    • reproduction_steps.log — full script output (both idempotent runs).
    • fixed-variant_variant_result.json — fixed v3.0.2: ssrfConfirmed:true, control_direct_loopback.blocked:true, variant_redirect.ssrf:true, downloadedContent:"VARIANT_SSRF_MARKER_...", victim_hits:[{remote:"127.0.0.1",path:"/",host:"127.0.0.1:18080"}].
    • fixed-variant_victim_server.log[VICTIM:18080] Request from 127.0.0.1 path=/ host=127.0.0.1:18080.
    • fixed-variant_variant_test.log — MCP JSON-RPC trace; control file error "Refusing to fetch 127.0.0.1 ...", variant file localPath/size:42 (downloaded marker).
    • main-variant_variant_result.json / vuln-variant_variant_result.json — same outcome on latest main and on vulnerable v3.0.1.
    • redirect_tier1.log / redirect_tier1.json — mechanism proof with Playwright request.newContext().get() (the same HTTP client as ctx.request.get): assertSafeUrl(initial) PASS, ctx.request.get followed 302 to http://127.0.0.1:18080/, REDIRECT_FOLLOWED_TO_LOOPBACK: true, MARKER_IN_BODY: true.
    • probe_guard.log — guard-logic matrix across candidate addresses (shows ::ffff:0:127.0.0.1 etc. bypass the fixed guard's string check).
    • routing_test.log — empirical TCP routing test (shows non-mapped IPv4-in-IPv6 forms are ENETUNREACH, i.e. not a working SSRF).
  • Key excerpts (fixed v3.0.2):
    • Tool result: {"status":"ok","downloaded":1,"total":2,"files":[{"url":"http://127.0.0.1:18080/direct-control","error":"Refusing to fetch 127.0.0.1 (resolves to private/loopback/link-local address 127.0.0.1)..."},{"url":"http://httpbin.org/redirect-to?...#v","localPath":".../file-1.bin","size":42}]}
    • Victim log: [VICTIM:18080] Request from 127.0.0.1 path=/ host=127.0.0.1:18080
  • Environment: Node v24.18.0; Playwright chromium headless shell-1208; Ubuntu container (eth0 172.19.0.7); outbound internet enabled (httpbin.org resolves to public IPs and returns 302).

Recommendations / Next Steps

To close the gap and ship a complete SSRF fix:

  1. Re-validate every fetched URL, including redirect targets. Preferred options:
    • Disable auto-redirect at the guard boundary and resolve redirects manually, calling assertSafeUrl() on each Location before following it; or
    • Wrap ctx.request.get()/page.goto() so that on each redirect the target URL is passed back through assertSafeUrl(); or
    • Pin the connection to the resolved IP from the guard's dns.lookup and reject any redirect whose target hostname resolves to a private/loopback/link-local address.
  2. Broaden isPrivateV6() IPv4-in-IPv6 handling (defense in depth): reject all IPv4-embedding forms — mapped (::ffff:x:y), translated (::ffff:0:x:y), compatible (::x:y), and NAT64 (64:ff9b::/96, 64:ff9b:1::/96) — by parsing with a real IP library rather than the groups.length === 2 heuristic. Reconstruct and run isPrivateV4() on the embedded IPv4 for each recognized prefix.
  3. Fix the link-local prefix check: use fe80::/10 (match fe80:fef0:) instead of only fe80:.
  4. Address DNS rebinding: consider resolving once, pinning the address, and connecting to the IP directly with the original Host header (or re-resolving and re-checking at fetch time).
  5. Add tests: end-to-end SSRF tests covering (a) public-URL-redirects-to-private for both download_media and auth_fetch, (b) all IPv4-in-IPv6 forms, (c) all private ranges.

Additional Notes

  • Idempotency: reproduction_steps.sh was run twice consecutively; both runs completed with identical results (vuln=true, fixed=true, main=true, exit 0). Worktrees and dist are reused across runs; per-run state (HOME dirs, markers, result files) is overwritten each run.
  • Bypass vs alternate trigger: this is a bypass — it reproduces on the patched/fixed code path (v3.0.2 and latest main), not only on the vulnerable version.
  • Trust boundary: the malicious URL crosses the MCP client→server trust boundary (tool argument over the JSON-RPC API); the server then fetches an internal/loopback resource it is documented to refuse. No SECURITY.md excludes this attack class; the product's own assertSafeUrl guard establishes that blocking private IPs is intended behavior.
  • Scope honesty: the same-root-cause class (IPv4-in-IPv6 hex normalization) variants ::ffff:0:127.0.0.1, ::127.0.0.1, and 64:ff9b::127.0.0.1 were tested and do bypass the fixed guard's string check, but they are not a working SSRF in this environment (ENETUNREACH — the kernel does not route non-mapped IPv4-in-IPv6 to IPv4). They are reported as a guard-incompleteness/defense-in-depth gap, not as a confirmed bypass. The confirmed bypass is the redirect-following path.
  • auth_fetch parity: the same redirect gap affects auth_fetch via browser.ts:58page.goto() (page.goto follows redirects/navigations). Runtime proof focused on download_media because it does not require the interactive capture-button click; the code-path analysis establishes auth_fetch is equally affected.

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.

Event 1/40
0:000:56
0:00
session startedaccounts/fireworks/routers/glm-5p2-fast · CVE-2026-49857 · REPRO-20
0:05
0:08
web search
0:09
web search
0:11
0:13
0:14
web search
0:16
0:18
0:28
0:28
extract_facts
no facts extracted
0:30
0:30
0:30
supportrepro
0:39
0:41
0:41
0:41
0:43
0:43
0:43
0:48
0:48
0:48
$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/null
total 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---
0:54
0:54
$node --version 2>/dev/null; npm --version 2>/dev/null; which node; which npm; echo "---"; git --version
v24.18.0
11.16.0
/usr/bin/node
/usr/bin/npm
---
git version 2.55.0
0:54
0:56

Artifacts and Evidence for CVE-2026-49857

Scripts, logs, diffs, and output captured during the reproduction.

bundle/ticket.md0.9 KB
bundle/ticket.json1.4 KB
bundle/repro/mcp_client.js6.8 KB
bundle/repro/runtime_manifest.json1.3 KB
bundle/repro/validation_verdict.json0.8 KB
bundle/logs/vulnerable_test.log2.2 KB
bundle/logs/vulnerable_marker.txt0.0 KB
bundle/logs/vulnerable_victim_server.log0.3 KB
bundle/logs/vulnerable_mcp_requests.log0.7 KB
bundle/logs/vulnerable_mcp_stdout.log2.6 KB
bundle/logs/vulnerable_result.json1.0 KB
bundle/logs/fixed_test.log2.0 KB
bundle/logs/fixed_marker.txt0.0 KB
bundle/logs/fixed_victim_server.log0.2 KB
bundle/logs/fixed_mcp_requests.log0.7 KB
bundle/logs/fixed_mcp_stdout.log2.7 KB
bundle/logs/fixed_result.json0.8 KB
bundle/logs/reproduction_steps.log7.3 KB
bundle/logs/mcp-home-vulnerable/.auth-fetch-mcp/downloads/2026-07-02T18-06-43/file-1.bin0.0 KB
bundle/logs/vuln_variant/probe_guard.log3.5 KB
bundle/logs/vuln_variant/routing_test.log1.6 KB
bundle/logs/vuln_variant/routing_test.json1.8 KB
bundle/logs/vuln_variant/redirect_tier1.log0.6 KB
bundle/logs/vuln_variant/redirect_tier1.json0.7 KB
bundle/logs/vuln_variant/fixed_variant_test.log1.5 KB
bundle/logs/vuln_variant/fixed-variant_marker.txt0.0 KB
bundle/logs/vuln_variant/mcp-home-vuln-variant/.auth-fetch-mcp/downloads/2026-07-02T18-15-57/file-1.bin0.0 KB
bundle/logs/vuln_variant/fixed_variant_result.json1.5 KB
bundle/logs/vuln_variant/main_variant_test.log1.5 KB
bundle/logs/vuln_variant/main-variant_marker.txt0.0 KB
bundle/logs/vuln_variant/main-variant_victim_server.log0.9 KB
bundle/logs/vuln_variant/main-variant_mcp_requests.log2.2 KB
bundle/logs/vuln_variant/main_variant_result.json1.5 KB
bundle/logs/vuln_variant/vuln_variant_test.log1.5 KB
bundle/logs/vuln_variant/vuln-variant_marker.txt0.0 KB
bundle/logs/vuln_variant/vuln-variant_victim_server.log0.9 KB
bundle/logs/vuln_variant/vuln-variant_mcp_requests.log2.2 KB
bundle/logs/vuln_variant/vuln_variant_result.json1.5 KB
bundle/logs/vuln_variant/fixed-variant_victim_server.log0.9 KB
bundle/logs/vuln_variant/fixed-variant_mcp_requests.log2.2 KB
bundle/logs/vuln_variant/reproduction_steps.log7.5 KB
bundle/logs/vuln_variant/vuln-variant_variant_test.log1.5 KB
bundle/logs/vuln_variant/vuln-variant_variant_result.json1.5 KB
bundle/logs/vuln_variant/fixed-variant_variant_test.log1.5 KB
bundle/logs/vuln_variant/fixed-variant_variant_result.json1.5 KB
bundle/logs/vuln_variant/main-variant_variant_test.log1.5 KB
bundle/logs/vuln_variant/main-variant_variant_result.json1.5 KB
bundle/logs/vuln_variant/final_run.log7.5 KB
bundle/logs/vuln_variant/mcp-home-fixed-variant/.auth-fetch-mcp/downloads/2026-07-02T18-15-59/file-1.bin0.0 KB
bundle/logs/vuln_variant/mcp-home-main-variant/.auth-fetch-mcp/downloads/2026-07-02T18-16-02/file-1.bin0.0 KB
bundle/vuln_variant/probe_guard.js4.9 KB
bundle/vuln_variant/probe_routing.js3.0 KB
bundle/vuln_variant/redirect_tier1.js3.6 KB
bundle/vuln_variant/variant_mcp_client.js8.7 KB
bundle/vuln_variant/runtime_manifest.json1.5 KB
bundle/vuln_variant/patch_analysis.md8.6 KB
bundle/vuln_variant/variant_manifest.json5.5 KB
bundle/vuln_variant/validation_verdict.json4.3 KB
bundle/vuln_variant/source_identity.json2.8 KB
bundle/vuln_variant/root_cause_equivalence.json4.6 KB
bundle/repro/reproduction_steps.sh12.0 KB
bundle/repro/rca_report.md10.3 KB
bundle/vuln_variant/reproduction_steps.sh10.3 KB
bundle/vuln_variant/rca_report.md12.8 KB
08 · How to Fix

How to Fix CVE-2026-49857

Upgrade ymw0407/auth-fetch-mcp · npm to 3.0.2 or later.

Coming soon

Step-by-step mitigation and hardening guidance for CVE-2026-49857 — configuration checks, workarounds where no patch exists, and how to verify you're protected — is on the way.

10 · FAQ

FAQ: CVE-2026-49857

How does the CVE-2026-49857 SSRF bypass work in practice?

An attacker (via a malicious MCP client or prompt injection) supplies a URL such as http://[::ffff:127.0.0.1]:PORT/ to the auth_fetch or download_media MCP tool. The bypassed guard lets the server fetch arbitrary internal/loopback URLs; download_media downloads the fetched content to disk and returns the file path, enabling information disclosure from internal services.

Which auth-fetch-mcp versions are affected by CVE-2026-49857, and where is it fixed?

Versions <= 3.0.1 are affected. It is fixed in 3.0.2.

How severe is CVE-2026-49857?

It is rated high severity: the guard bypass lets an attacker cause the MCP server to fetch and return content from arbitrary internal or loopback endpoints, such as cloud metadata services.

How can I reproduce CVE-2026-49857?

Download the verified script from this page and run it in an isolated environment against auth-fetch-mcp@3.0.1 with default settings. Invoke the auth_fetch or download_media tool with URL http://[::ffff:127.0.0.1]:<PORT>/ and confirm the server fetches the loopback URL and returns the response; confirm 3.0.2 blocks the request.
11 · References

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.