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:
74
internal/handlers/media_handler_test.go
Normal file
74
internal/handlers/media_handler_test.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/treytartt/casera-api/internal/config"
|
||||
"github.com/treytartt/casera-api/internal/services"
|
||||
)
|
||||
|
||||
// newTestStorageService creates a StorageService with a known upload directory for testing.
|
||||
// It does NOT call NewStorageService because that creates directories on disk.
|
||||
// Instead, it directly constructs the struct with only what resolveFilePath needs.
|
||||
func newTestStorageService(uploadDir string) *services.StorageService {
|
||||
cfg := &config.StorageConfig{
|
||||
UploadDir: uploadDir,
|
||||
BaseURL: "/uploads",
|
||||
MaxFileSize: 10 * 1024 * 1024,
|
||||
AllowedTypes: "image/jpeg,image/png",
|
||||
}
|
||||
// Use the exported constructor helper that skips directory creation (for tests)
|
||||
return services.NewStorageServiceForTest(cfg)
|
||||
}
|
||||
|
||||
func TestResolveFilePath_NormalPath_Works(t *testing.T) {
|
||||
storageSvc := newTestStorageService("/var/uploads")
|
||||
h := NewMediaHandler(nil, nil, nil, storageSvc)
|
||||
|
||||
result := h.resolveFilePath("images/photo.jpg")
|
||||
require.NotEmpty(t, result)
|
||||
assert.Equal(t, "/var/uploads/images/photo.jpg", result)
|
||||
}
|
||||
|
||||
func TestResolveFilePath_LegacyUploadPath_Works(t *testing.T) {
|
||||
storageSvc := newTestStorageService("/var/uploads")
|
||||
h := NewMediaHandler(nil, nil, nil, storageSvc)
|
||||
|
||||
result := h.resolveFilePath("/uploads/images/photo.jpg")
|
||||
require.NotEmpty(t, result)
|
||||
assert.Equal(t, "/var/uploads/images/photo.jpg", result)
|
||||
}
|
||||
|
||||
func TestResolveFilePath_DotDotTraversal_Blocked(t *testing.T) {
|
||||
storageSvc := newTestStorageService("/var/uploads")
|
||||
h := NewMediaHandler(nil, nil, nil, storageSvc)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
storedURL string
|
||||
}{
|
||||
{"simple dotdot", "../etc/passwd"},
|
||||
{"nested dotdot", "../../etc/shadow"},
|
||||
{"embedded dotdot", "images/../../etc/passwd"},
|
||||
{"legacy prefix with dotdot", "/uploads/../../../etc/passwd"},
|
||||
{"deep dotdot", "a/b/c/../../../../etc/passwd"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := h.resolveFilePath(tt.storedURL)
|
||||
assert.Empty(t, result, "path traversal should return empty string for: %s", tt.storedURL)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveFilePath_EmptyURL_ReturnsEmpty(t *testing.T) {
|
||||
storageSvc := newTestStorageService("/var/uploads")
|
||||
h := NewMediaHandler(nil, nil, nil, storageSvc)
|
||||
|
||||
result := h.resolveFilePath("")
|
||||
assert.Empty(t, result)
|
||||
}
|
||||
Reference in New Issue
Block a user