fix: add panic recovery, rate limiting, timing-safe CI token
- Add Recovery middleware (catches panics, returns 500, logs stack trace) - Add RateLimiter to middleware chain (30 req/min, burst 60 per IP) - Fix CI token comparison with subtle.ConstantTimeCompare (timing attack) - Middleware chain: Recovery → Logging → RateLimit → CORS → mux Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -67,10 +67,13 @@ func main() {
|
|||||||
templatesHandler := templates.NewHandler(db, cfg)
|
templatesHandler := templates.NewHandler(db, cfg)
|
||||||
templatesHandler.RegisterRoutes(mux)
|
templatesHandler.RegisterRoutes(mux)
|
||||||
|
|
||||||
// Wrapper chain: Logging → CORS → mux.
|
// Wrapper chain: Recovery → Logging → RateLimit → CORS → mux.
|
||||||
|
// Recovery must be outermost so it catches panics in all inner layers.
|
||||||
var handler http.Handler = mux
|
var handler http.Handler = mux
|
||||||
handler = middleware.CORS(handler)
|
handler = middleware.CORS(handler)
|
||||||
|
handler = middleware.NewRateLimiter(30, time.Minute, 60).Limit(handler)
|
||||||
handler = middleware.Logging(handler)
|
handler = middleware.Logging(handler)
|
||||||
|
handler = middleware.Recovery(handler)
|
||||||
|
|
||||||
addr := ":" + itoa(cfg.Port)
|
addr := ":" + itoa(cfg.Port)
|
||||||
srv := &http.Server{
|
srv := &http.Server{
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"crypto/sha1"
|
"crypto/sha1"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
|
"crypto/subtle"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -85,7 +86,7 @@ func (h *Handler) auth(next http.HandlerFunc) http.HandlerFunc {
|
|||||||
func (h *Handler) ciToken(next http.HandlerFunc) http.HandlerFunc {
|
func (h *Handler) ciToken(next http.HandlerFunc) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
token := r.Header.Get("X-CI-Token")
|
token := r.Header.Get("X-CI-Token")
|
||||||
if token == "" || token != h.cfg.CIsecret {
|
if token == "" || subtle.ConstantTimeCompare([]byte(token), []byte(h.cfg.CIsecret)) != 1 {
|
||||||
writeError(w, http.StatusForbidden, "Invalid CI token")
|
writeError(w, http.StatusForbidden, "Invalid CI token")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ package middleware
|
|||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"runtime/debug"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -39,6 +40,20 @@ func Logging(next http.Handler) http.Handler {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Recovery catches panics in downstream handlers and returns 500.
|
||||||
|
// Logs the stack trace for debugging.
|
||||||
|
func Recovery(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
defer func() {
|
||||||
|
if rec := recover(); rec != nil {
|
||||||
|
log.Printf("PANIC: %v\n%s", rec, debug.Stack())
|
||||||
|
http.Error(w, `{"error":"Internal Server Error"}`, http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// statusWriter wraps http.ResponseWriter to capture the status code.
|
// statusWriter wraps http.ResponseWriter to capture the status code.
|
||||||
type statusWriter struct {
|
type statusWriter struct {
|
||||||
http.ResponseWriter
|
http.ResponseWriter
|
||||||
|
|||||||
Reference in New Issue
Block a user