fix: final vulnerability sweep — storage auth, security headers, body limits, cookie flags

- 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
This commit is contained in:
Claus Lohmar 2026-05-31 02:01:12 +00:00
parent e831fcf617
commit a7381bde0c
3 changed files with 29 additions and 9 deletions

View file

@ -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)
}

View file

@ -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),
})

28
main.go
View file

@ -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)
})
// -----------------------------------------------------------------------