S35: lint scripts — secret-logs (blocking) + csrf (review-aid)
- SHA
a894fbba660786d1680c9c1f76207a2e017efa96- Parents
-
5be109c - Tree
a0e4b5b
a894fbb
a894fbba660786d1680c9c1f76207a2e017efa965be109c
a0e4b5b| Status | File | + | - |
|---|---|---|---|
| A |
scripts/lint-csrf.sh
|
66 | 0 |
| A |
scripts/lint-secret-logs.sh
|
74 | 0 |
scripts/lint-csrf.shadded@@ -0,0 +1,66 @@ | ||
| 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)" | |
scripts/lint-secret-logs.shadded@@ -0,0 +1,74 @@ | ||
| 1 | +#!/usr/bin/env bash | |
| 2 | +# SPDX-License-Identifier: AGPL-3.0-or-later | |
| 3 | +# | |
| 4 | +# Fail when source code emits log lines containing token-prefix | |
| 5 | +# patterns that would leak credentials into operator logs. The | |
| 6 | +# canonical bad shape: | |
| 7 | +# | |
| 8 | +# logger.Info("got authorization header", "auth", r.Header.Get("Authorization")) | |
| 9 | +# | |
| 10 | +# Even when the value happens to be empty, the format string itself | |
| 11 | +# is a smell — anyone who edits the line to format the value back in | |
| 12 | +# will silently start logging the secret. | |
| 13 | +# | |
| 14 | +# Patterns we reject (case-insensitive substring match in *.go): | |
| 15 | +# - "shithub_pat_" — PAT token prefix | |
| 16 | +# - "otpauth://" — TOTP URI containing the secret | |
| 17 | +# - "Authorization:" — header literal that suggests dumping headers | |
| 18 | +# | |
| 19 | +# Carve-outs (whitelisted paths) live in EXEMPTS below. The S08 PAT | |
| 20 | +# redactor for git-clone URLs and the auth-flow tests are the legit | |
| 21 | +# call sites; everything else is suspicious. | |
| 22 | +# | |
| 23 | +# Run as part of `make ci`. | |
| 24 | + | |
| 25 | +set -euo pipefail | |
| 26 | + | |
| 27 | +cd "$(git rev-parse --show-toplevel)" | |
| 28 | + | |
| 29 | +EXEMPTS=( | |
| 30 | + # S08 redactor explicitly handles the prefix to strip it from URLs. | |
| 31 | + "internal/auth/pat/" | |
| 32 | + # Log redactor IS the canonical handler for the secret patterns. | |
| 33 | + "internal/infra/log/log.go" | |
| 34 | + # TOTP package builds the otpauth:// URI for the QR provisioning | |
| 35 | + # code — never logs it, just constructs. | |
| 36 | + "internal/auth/totp/totp.go" | |
| 37 | + # PAT middleware parses (not logs) the Authorization header. | |
| 38 | + "internal/web/middleware/pat.go" | |
| 39 | + # Git-HTTP auth handler mentions the prefix in doc comments. | |
| 40 | + "internal/web/handlers/githttp/auth.go" | |
| 41 | + # Tests legitimately mention these strings in fixtures. | |
| 42 | + "_test.go" | |
| 43 | + # The lint script itself documents the patterns. | |
| 44 | + "scripts/lint-secret-logs.sh" | |
| 45 | +) | |
| 46 | + | |
| 47 | +RE='shithub_pat_|otpauth://|Authorization:' | |
| 48 | + | |
| 49 | +# grep -EIn: extended regex, ignore binary, show line numbers. | |
| 50 | +# Search only shithub-owned trees — .refs/ vendored repos are docs. | |
| 51 | +matches=$(grep -RIEn --include='*.go' "$RE" cmd internal scripts 2>/dev/null || true) | |
| 52 | + | |
| 53 | +filtered="" | |
| 54 | +while IFS= read -r line; do | |
| 55 | + [ -z "$line" ] && continue | |
| 56 | + skip=false | |
| 57 | + for pat in "${EXEMPTS[@]}"; do | |
| 58 | + if [[ "$line" == *"$pat"* ]]; then | |
| 59 | + skip=true | |
| 60 | + break | |
| 61 | + fi | |
| 62 | + done | |
| 63 | + $skip || filtered="${filtered}${line}"$'\n' | |
| 64 | +done <<< "$matches" | |
| 65 | + | |
| 66 | +if [ -n "${filtered// /}" ]; then | |
| 67 | + echo "lint-secret-logs: token-prefix patterns found in non-exempt source:" >&2 | |
| 68 | + echo "$filtered" >&2 | |
| 69 | + echo >&2 | |
| 70 | + echo "If this is a legitimate use, add the file to EXEMPTS in scripts/lint-secret-logs.sh." >&2 | |
| 71 | + exit 1 | |
| 72 | +fi | |
| 73 | + | |
| 74 | +echo "lint-secret-logs: ok" | |