From a7381bde0c6b8384d151e5de7c55b6bcf6ea28b0 Mon Sep 17 00:00:00 2001 From: cclohmar Date: Sun, 31 May 2026 02:01:12 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20final=20vulnerability=20sweep=20?= =?UTF-8?q?=E2=80=94=20storage=20auth,=20security=20headers,=20body=20limi?= =?UTF-8?q?ts,=20cookie=20flags?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Storage route moved behind auth middleware (was publicly accessible) - Security headers: X-Content-Type-Options, X-Frame-Options, CSP, Referrer-Policy - Request body size limit: 10 MB on all endpoints via MaxBytesReader - Session cookie now sets Secure flag when BASE_URL uses HTTPS - readFile() returns proper errors for dirs & oversized files (was nil,nil) - Removed dead DEEPSEEK_API_KEY code from main.go - Added fmt import to ai/receipt.go for error formatting --- internal/ai/receipt.go | 5 +++-- internal/handlers/auth.go | 5 ++++- main.go | 28 ++++++++++++++++++++++------ 3 files changed, 29 insertions(+), 9 deletions(-) diff --git a/internal/ai/receipt.go b/internal/ai/receipt.go index e4ee498..bbcbdc8 100644 --- a/internal/ai/receipt.go +++ b/internal/ai/receipt.go @@ -9,6 +9,7 @@ package ai import ( + "fmt" "os" "strings" ) @@ -72,10 +73,10 @@ func readFile(path string) ([]byte, error) { return nil, err } if info.IsDir() { - return nil, err + return nil, fmt.Errorf("readFile: %q is a directory, not a file", path) } if info.Size() > 10<<20 { - return nil, err + return nil, fmt.Errorf("readFile: %q exceeds 10 MB limit", path) } return os.ReadFile(path) } diff --git a/internal/handlers/auth.go b/internal/handlers/auth.go index 7745c59..759fb50 100644 --- a/internal/handlers/auth.go +++ b/internal/handlers/auth.go @@ -9,6 +9,7 @@ import ( "html/template" "log" "net/http" + "os" "strings" "sync" "time" @@ -199,13 +200,15 @@ func (h *AuthHandler) VerifyOTP(w http.ResponseWriter, r *http.Request) { return } - // Set the HTTP-only session cookie with a 24-hour TTL. + // Set the session cookie (HttpOnly, SameSite=Lax, Secure, 24h). + secure := strings.HasPrefix(os.Getenv("BASE_URL"), "https://") http.SetCookie(w, &http.Cookie{ Name: "session_token", Value: token, Path: "/", HttpOnly: true, SameSite: http.SameSiteLaxMode, + Secure: secure, Expires: time.Now().Add(24 * time.Hour), }) diff --git a/main.go b/main.go index a0fd924..8338ea1 100644 --- a/main.go +++ b/main.go @@ -50,9 +50,6 @@ func main() { smtpUser := os.Getenv("SMTP_USER") smtpPass := os.Getenv("SMTP_PASS") - // DeepSeek API key is read directly by the ai package. - _ = os.Getenv("DEEPSEEK_API_KEY") - // ----------------------------------------------------------------------- // Database // ----------------------------------------------------------------------- @@ -109,6 +106,25 @@ func main() { r.Use(middleware.Logger) r.Use(middleware.Recoverer) r.Use(middleware.RealIP) + // Request body size limit on all endpoints (10 MB). + r.Use(func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + r.Body = http.MaxBytesReader(w, r.Body, 10<<20) + next.ServeHTTP(w, r) + }) + }) + + // Security headers. + r.Use(func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("X-Content-Type-Options", "nosniff") + w.Header().Set("X-Frame-Options", "DENY") + w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin") + w.Header().Set("Content-Security-Policy", + "default-src 'self'; img-src 'self' data:; script-src 'self' https://unpkg.com; style-src 'self' 'unsafe-inline'") + next.ServeHTTP(w, r) + }) + }) // PWA headers for service worker. r.Use(func(next http.Handler) http.Handler { @@ -147,9 +163,6 @@ func main() { http.ServeFile(w, r, "static/favicon.svg") })) - // Serve uploaded receipt images. - r.Get("/storage/*", http.StripPrefix("/storage/", http.FileServer(http.Dir("storage"))).ServeHTTP) - // ---- Public routes (no auth required) ---- r.Get("/", authHandler.LandingPage) @@ -179,6 +192,9 @@ func main() { // Filing. r.Post("/events/{id}/file", fileHandler.FileEvent) + + // Storage (receipt images) — protected by auth middleware. + r.Get("/storage/*", http.StripPrefix("/storage/", http.FileServer(http.Dir("storage"))).ServeHTTP) }) // -----------------------------------------------------------------------