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:
parent
e831fcf617
commit
a7381bde0c
3 changed files with 29 additions and 9 deletions
|
|
@ -9,6 +9,7 @@
|
||||||
package ai
|
package ai
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
@ -72,10 +73,10 @@ func readFile(path string) ([]byte, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if info.IsDir() {
|
if info.IsDir() {
|
||||||
return nil, err
|
return nil, fmt.Errorf("readFile: %q is a directory, not a file", path)
|
||||||
}
|
}
|
||||||
if info.Size() > 10<<20 {
|
if info.Size() > 10<<20 {
|
||||||
return nil, err
|
return nil, fmt.Errorf("readFile: %q exceeds 10 MB limit", path)
|
||||||
}
|
}
|
||||||
return os.ReadFile(path)
|
return os.ReadFile(path)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"html/template"
|
"html/template"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
@ -199,13 +200,15 @@ func (h *AuthHandler) VerifyOTP(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
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{
|
http.SetCookie(w, &http.Cookie{
|
||||||
Name: "session_token",
|
Name: "session_token",
|
||||||
Value: token,
|
Value: token,
|
||||||
Path: "/",
|
Path: "/",
|
||||||
HttpOnly: true,
|
HttpOnly: true,
|
||||||
SameSite: http.SameSiteLaxMode,
|
SameSite: http.SameSiteLaxMode,
|
||||||
|
Secure: secure,
|
||||||
Expires: time.Now().Add(24 * time.Hour),
|
Expires: time.Now().Add(24 * time.Hour),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
28
main.go
28
main.go
|
|
@ -50,9 +50,6 @@ func main() {
|
||||||
smtpUser := os.Getenv("SMTP_USER")
|
smtpUser := os.Getenv("SMTP_USER")
|
||||||
smtpPass := os.Getenv("SMTP_PASS")
|
smtpPass := os.Getenv("SMTP_PASS")
|
||||||
|
|
||||||
// DeepSeek API key is read directly by the ai package.
|
|
||||||
_ = os.Getenv("DEEPSEEK_API_KEY")
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
// Database
|
// Database
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
|
|
@ -109,6 +106,25 @@ func main() {
|
||||||
r.Use(middleware.Logger)
|
r.Use(middleware.Logger)
|
||||||
r.Use(middleware.Recoverer)
|
r.Use(middleware.Recoverer)
|
||||||
r.Use(middleware.RealIP)
|
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.
|
// PWA headers for service worker.
|
||||||
r.Use(func(next http.Handler) http.Handler {
|
r.Use(func(next http.Handler) http.Handler {
|
||||||
|
|
@ -147,9 +163,6 @@ func main() {
|
||||||
http.ServeFile(w, r, "static/favicon.svg")
|
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) ----
|
// ---- Public routes (no auth required) ----
|
||||||
|
|
||||||
r.Get("/", authHandler.LandingPage)
|
r.Get("/", authHandler.LandingPage)
|
||||||
|
|
@ -179,6 +192,9 @@ func main() {
|
||||||
|
|
||||||
// Filing.
|
// Filing.
|
||||||
r.Post("/events/{id}/file", fileHandler.FileEvent)
|
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)
|
||||||
})
|
})
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue