{
  "variant_id": "cve-2026-49857-redirect-ssrf-bypass",
  "created_at": "2026-07-02T18:15:00Z",
  "variant_summary": "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-validating the redirect target. A public URL that 302-redirects to a loopback/private URL bypasses the guard on the FIXED v3.0.2 server and on latest main. End-to-end confirmed: control direct-loopback URL is blocked ('Refusing to fetch 127.0.0.1...'), the public-302->loopback variant reaches the internal victim and downloads its secret marker.",
  "relation": "newer_version_sibling",
  "origin_kind": "pruva_variant",
  "repository": "https://github.com/ymw0407/auth-fetch-mcp",
  "submitted_target": {
    "target_kind": "git_ref",
    "commit_sha": "98f381d1298b6b7e7ff29d7a7851f18ea5f2364c",
    "version": "3.0.1",
    "ref": "v3.0.1",
    "display": "auth-fetch-mcp v3.0.1 (vulnerable, commit 98f381d)"
  },
  "variant_target": {
    "target_kind": "git_ref",
    "commit_sha": "d4dedaf55c1d39228dbed58807ea1f9fac1328e1",
    "version": "3.0.2",
    "ref": "v3.0.2",
    "display": "auth-fetch-mcp v3.0.2 (fixed, commit d4dedaf; fix 177ec5f) — bypass confirmed here"
  },
  "same_root_cause_confidence": 0.45,
  "same_surface_confidence": 0.9,
  "claimed_surface": "SSRF guard bypass in auth-fetch-mcp assertSafeUrl() via IPv4-mapped IPv6 hex normalization (CVE-2026-49857).",
  "validated_surface": "SSRF guard bypass via redirect-following: assertSafeUrl() validates only the initial URL; ctx.request.get()/page.goto() follow 3xx to private/loopback targets without re-validation. Same guard (src/security.ts), same tools (download_media/auth_fetch), same sink (Playwright fetch), same trust boundary (MCP tool URL argument -> server fetch of internal IP).",
  "required_entrypoint_kind": "api_remote",
  "required_entrypoint_detail": "MCP stdio JSON-RPC tools/call download_media (and equivalently auth_fetch) with a PUBLIC url that 302-redirects to a private/loopback url, e.g. http://httpbin.org/redirect-to?url=http://127.0.0.1:18080/&status_code=302",
  "attacker_controlled_input": "The `urls` array argument to the download_media tool (and the `url` argument to auth_fetch). Attacker supplies a public URL whose 3xx Location points at an internal/loopback address.",
  "trigger_path": "tools.ts:233 assertSafeUrl(url) -> passes (public host) -> tools.ts:234 ctx.request.get(safeUrl.toString()) -> server returns 302 Location: http://127.0.0.1:PORT/ -> ctx.request.get follows redirect with NO assertSafeUrl on target -> reaches loopback victim -> tools.ts writes downloaded file -> returns {downloaded:1, files:[{localPath,size}]}. Control URL http://127.0.0.1:PORT/direct-control in same call is correctly blocked by assertSafeUrl.",
  "observed_impact_class": "server_side_request_forgery",
  "exploitability_confidence": 0.9,
  "evidence_scope": "end_to_end_runtime_on_fixed_and_latest",
  "runtime_manifest_present": true,
  "end_to_end_target_reached": true,
  "inferred": false,
  "file_path": "src/tools.ts",
  "line_start": 233,
  "line_end": 234,
  "secondary_anchors": [
    {
      "file_path": "src/browser.ts",
      "line_start": 58,
      "line_end": 66
    },
    {
      "file_path": "src/security.ts",
      "line_start": 87,
      "line_end": 120
    }
  ],
  "review_scope_paths": [
    "src/security.ts",
    "src/tools.ts",
    "src/browser.ts",
    "src/extractor.ts",
    "src/index.ts"
  ],
  "blocking_mitigation": "None on the fixed version. The fix 177ec5f only hardens isPrivateV6() hex normalization; it does not re-validate redirect targets. AUTH_FETCH_ALLOW_PRIVATE is opt-in and off by default. No redirect re-validation exists in any tested ref (v3.0.1, v3.0.2, origin/main).",
  "claim_block_reason": null,
  "artifact_refs": {
    "variant_manifest": "bundle/vuln_variant/variant_manifest.json",
    "validation_verdict": "bundle/vuln_variant/validation_verdict.json",
    "runtime_manifest": "bundle/vuln_variant/runtime_manifest.json",
    "repro_log": "bundle/logs/vuln_variant/reproduction_steps.log",
    "root_cause_equivalence": "bundle/vuln_variant/root_cause_equivalence.json",
    "source_identity": "bundle/vuln_variant/source_identity.json",
    "reproducer": [
      "bundle/vuln_variant/reproduction_steps.sh",
      "bundle/vuln_variant/variant_mcp_client.js",
      "bundle/vuln_variant/redirect_tier1.js",
      "bundle/vuln_variant/probe_guard.js",
      "bundle/vuln_variant/probe_routing.js"
    ]
  },
  "tested_refs": {
    "vulnerable": { "ref": "v3.0.1", "commit_sha": "98f381d1298b6b7e7ff29d7a7851f18ea5f2364c", "bypass_reproduced": true },
    "fixed": { "ref": "v3.0.2", "commit_sha": "d4dedaf55c1d39228dbed58807ea1f9fac1328e1", "fix_commit": "177ec5f8ee9c2d5749035777e562f699971b0da9", "bypass_reproduced": true },
    "latest": { "ref": "origin/main", "commit_sha": "a4b92452dc9332fb4063225f3d8842f3602a54a3", "bypass_reproduced": true }
  },
  "notes": "Same-root-cause CLASS (IPv4-in-IPv6 hex normalization) variants ::ffff:0:127.0.0.1, ::127.0.0.1, 64:ff9b::127.0.0.1 were also tested: they bypass the fixed guard's string check but do NOT route to loopback (ENETUNREACH), so they are guard-incompleteness/defense-in-depth gaps, not a working SSRF in this environment. The confirmed, exploitable bypass is the redirect-following path. See patch_analysis.md GAP 1/GAP 2."
}
