Harden API security: input validation, safe auth extraction, new tests, and deploy config

Comprehensive security hardening from audit findings:
- Add validation tags to all DTO request structs (max lengths, ranges, enums)
- Replace unsafe type assertions with MustGetAuthUser helper across all handlers
- Remove query-param token auth from admin middleware (prevents URL token leakage)
- Add request validation calls in handlers that were missing c.Validate()
- Remove goroutines in handlers (timezone update now synchronous)
- Add sanitize middleware and path traversal protection (path_utils)
- Stop resetting admin passwords on migration restart
- Warn on well-known default SECRET_KEY
- Add ~30 new test files covering security regressions, auth safety, repos, and services
- Add deploy/ config, audit digests, and AUDIT_FINDINGS documentation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-03-02 09:48:01 -06:00
parent 56d6fa4514
commit 7690f07a2b
123 changed files with 8321 additions and 750 deletions

View File

@@ -1,7 +1,6 @@
package handlers
import (
"path/filepath"
"strconv"
"strings"
@@ -9,7 +8,6 @@ import (
"github.com/treytartt/casera-api/internal/apperrors"
"github.com/treytartt/casera-api/internal/middleware"
"github.com/treytartt/casera-api/internal/models"
"github.com/treytartt/casera-api/internal/repositories"
"github.com/treytartt/casera-api/internal/services"
)
@@ -40,7 +38,10 @@ func NewMediaHandler(
// ServeDocument serves a document file with access control
// GET /api/media/document/:id
func (h *MediaHandler) ServeDocument(c echo.Context) error {
user := c.Get(middleware.AuthUserKey).(*models.User)
user, err := middleware.MustGetAuthUser(c)
if err != nil {
return err
}
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
@@ -73,7 +74,10 @@ func (h *MediaHandler) ServeDocument(c echo.Context) error {
// ServeDocumentImage serves a document image with access control
// GET /api/media/document-image/:id
func (h *MediaHandler) ServeDocumentImage(c echo.Context) error {
user := c.Get(middleware.AuthUserKey).(*models.User)
user, err := middleware.MustGetAuthUser(c)
if err != nil {
return err
}
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
@@ -111,7 +115,10 @@ func (h *MediaHandler) ServeDocumentImage(c echo.Context) error {
// ServeCompletionImage serves a task completion image with access control
// GET /api/media/completion-image/:id
func (h *MediaHandler) ServeCompletionImage(c echo.Context) error {
user := c.Get(middleware.AuthUserKey).(*models.User)
user, err := middleware.MustGetAuthUser(c)
if err != nil {
return err
}
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
@@ -152,7 +159,9 @@ func (h *MediaHandler) ServeCompletionImage(c echo.Context) error {
return c.File(filePath)
}
// resolveFilePath converts a stored URL to an actual file path
// resolveFilePath converts a stored URL to an actual file path.
// Returns empty string if the URL is empty or the resolved path would escape
// the upload directory (path traversal attempt).
func (h *MediaHandler) resolveFilePath(storedURL string) string {
if storedURL == "" {
return ""
@@ -160,12 +169,18 @@ func (h *MediaHandler) resolveFilePath(storedURL string) string {
uploadDir := h.storageSvc.GetUploadDir()
// Handle legacy /uploads/... URLs
// Strip legacy /uploads/ prefix to get relative path
relativePath := storedURL
if strings.HasPrefix(storedURL, "/uploads/") {
relativePath := strings.TrimPrefix(storedURL, "/uploads/")
return filepath.Join(uploadDir, relativePath)
relativePath = strings.TrimPrefix(storedURL, "/uploads/")
}
// Handle relative paths (new format)
return filepath.Join(uploadDir, storedURL)
// Use SafeResolvePath to validate containment within upload directory
resolved, err := services.SafeResolvePath(uploadDir, relativePath)
if err != nil {
// Path traversal or invalid path — return empty to signal file not found
return ""
}
return resolved
}