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" | ||