Files
member-portal/sweep.sh
T
inference-bot 5720a63958 security: separate admin token, verify credit-pack orders, revoke access on lapse
Member disclosure, three findings (fixes 1-3 of 5):

1. WEBHOOK_TOKEN was both the Open Collective webhook URL token and the
   X-Admin-Token for every /admin/* route, so anything that learned the webhook
   URL (OC admins, OC logs, this app's access logs) gained portal admin. The URL
   token is forced by OC's generic webhooks, which cannot set headers — the reuse
   is not. Introduce ADMIN_TOKEN, header-only. ADMIN_TOKEN_LEGACY=1 (default)
   still accepts WEBHOOK_TOKEN and logs a warning, so the admin panel and the
   scheduler keep working; set ADMIN_TOKEN everywhere, then flip it to 0 and
   rotate. sweep.sh and the digest script now send ADMIN_TOKEN.

2. The credit-pack path fell back to the webhook payload's own amount when OC
   could not resolve the order, so a forged POST naming its own totalAmount
   minted credits for any slug. The payload is now a hint only: an unresolvable
   order is refused, logged loudly, and emailed to the operator for manual
   reconciliation. Transient OC failures hold instead of granting blind.

3. Lapsing did not revoke access: both deactivation sites (lapse webhook, nightly
   sweep) discarded the key token deactivate_member() returns. Member keys carry
   no max_budget of their own, so they inherit the team's — zero the team budget
   on deactivation, which blocks every key under it. Key deletion is deliberately
   NOT used: provisioning reuses the stored sk- token on reactivation, and
   get_or_create_team does not update an existing team's budget, so the two
   renewal paths now re-sync the budget explicitly. Failures are loud (log +
   alert email); deactivation still completes.
2026-10-27 03:31:40 +00:00

42 lines
1.4 KiB
Bash

#!/bin/sh
# Membership reconciliation sweep — run by the Cloudron scheduler addon.
#
# Calls the portal's own /admin/sweep endpoint (localhost), which reconciles
# the portal against the live Open Collective member list: provisions missing
# members, deactivates lapsed members, and syncs Loomio.
#
# The ADMIN_TOKEN env var is shared with the main app (scheduler runs in the
# same launch environment), so we use it to authenticate the call. Admin auth is
# a header, never a URL token (security fix 2026-10-27). WEBHOOK_TOKEN is only a
# fallback until this caller is fully migrated.
#
# Uses Python (guaranteed present in the base image) rather than curl.
set -e
ADMIN_TOKEN="${ADMIN_TOKEN:-$WEBHOOK_TOKEN}"
if [ -z "$ADMIN_TOKEN" ]; then
echo "sweep: neither ADMIN_TOKEN nor WEBHOOK_TOKEN is set; aborting" >&2
exit 1
fi
python3 - "$ADMIN_TOKEN" <<'PY'
import sys, urllib.request, urllib.error
token = sys.argv[1]
url = "http://127.0.0.1:8000/admin/sweep"
req = urllib.request.Request(url, method="POST", data=b"", headers={
"Content-Type": "application/json",
"X-Admin-Token": token,
})
try:
with urllib.request.urlopen(req, timeout=60) as resp:
print("sweep:", resp.read().decode())
except urllib.error.HTTPError as e:
print(f"sweep: HTTP {e.code}: {e.read().decode()}", file=sys.stderr)
sys.exit(1)
except Exception as e:
print(f"sweep: request failed: {e}", file=sys.stderr)
sys.exit(1)
PY