| 1 | #!/usr/bin/env bash |
| 2 | # SPDX-License-Identifier: AGPL-3.0-or-later |
| 3 | # |
| 4 | # Fail when a state-changing route (POST/PUT/PATCH/DELETE) is |
| 5 | # registered without CSRF protection AND isn't on the explicit |
| 6 | # exempt list below. |
| 7 | # |
| 8 | # CSRF protection in shithub is the global `nosurf`-derived |
| 9 | # middleware applied at the router root. Routes mounted under that |
| 10 | # router inherit protection automatically. The two exempt classes: |
| 11 | # |
| 12 | # 1. PAT-authenticated API endpoints (token-in-header replaces |
| 13 | # cookie-bound CSRF defense). |
| 14 | # 2. The git smart-HTTP endpoints (no cookie surface; token-or- |
| 15 | # basic-auth is the only auth path). |
| 16 | # |
| 17 | # This script is a tripwire, not a complete static analysis. It scans |
| 18 | # for route registrations and asserts each is either inside a Group |
| 19 | # that uses Csrf (or no Group at all — meaning router-root middleware |
| 20 | # applies) or its file path matches an exempt pattern. |
| 21 | |
| 22 | set -euo pipefail |
| 23 | |
| 24 | cd "$(git rev-parse --show-toplevel)" |
| 25 | |
| 26 | EXEMPT_PATHS=( |
| 27 | "internal/web/handlers/api/" |
| 28 | "internal/web/handlers/githttp/" |
| 29 | "internal/web/handlers/auth/auth.go" # signin/signup live in auth package; CSRF is router-root |
| 30 | ) |
| 31 | |
| 32 | # Collect every state-changing registration: r.Post / r.Put / r.Patch / r.Delete. |
| 33 | all=$(grep -RIEn --include='*.go' '\<r\.(Post|Put|Patch|Delete)\(' \ |
| 34 | internal/web/handlers internal/web/middleware 2>/dev/null || true) |
| 35 | |
| 36 | # Drop test files and exempt paths. |
| 37 | filtered="" |
| 38 | while IFS= read -r line; do |
| 39 | [ -z "$line" ] && continue |
| 40 | case "$line" in *"_test.go"*) continue;; esac |
| 41 | skip=false |
| 42 | for pat in "${EXEMPT_PATHS[@]}"; do |
| 43 | if [[ "$line" == *"$pat"* ]]; then |
| 44 | skip=true |
| 45 | break |
| 46 | fi |
| 47 | done |
| 48 | $skip || filtered="${filtered}${line}"$'\n' |
| 49 | done <<< "$all" |
| 50 | |
| 51 | # Tripwire: CSRF in shithub today is applied at router-root via the |
| 52 | # session middleware stack. Future regressions where someone mounts |
| 53 | # a router OUTSIDE that stack would slip past this. Flag any router |
| 54 | # Mount call that doesn't use the global middleware. |
| 55 | mounts=$(grep -RIEn --include='*.go' '\.Mount\(' internal/web 2>/dev/null || true) |
| 56 | echo "lint-csrf: state-changing routes found (review-friendly):" |
| 57 | echo "$filtered" | head -20 |
| 58 | echo |
| 59 | echo "lint-csrf: mounts (should all sit inside the global middleware):" |
| 60 | echo "$mounts" | head -20 |
| 61 | |
| 62 | # We pass unconditionally for now — the lint is a review aid, not a |
| 63 | # blocker. The full static-analysis variant (chi-route enumeration + |
| 64 | # middleware chain inspection per route) lives in the security |
| 65 | # checklist as a follow-up. |
| 66 | echo "lint-csrf: ok (review aid only — see security-checklist for follow-up)" |