fix: critical security vulnerabilities
- Fix type assertion panics in auth.go (9 handlers)
* GetProfile, UpdateProfile, UpdateTheme, UpdateUsername
* UpdateEmail, UpdatePassword, DeleteAccount
* UpdateScanSettings, GetScanSettings, Register admin check
* Replace c.Get("user_id").(string) with MustGetAuthenticatedUser()
- Fix type assertion panic in library.go
* GetUserVisibleLibraries now uses MustGetAuthenticatedUser()
- Add path traversal protection to AddLibraryFolder
* Detect and block ".." in paths
* Clean paths with filepath.Clean()
* Verify path is a directory before adding
- Remove debug logging from Login handler
* Removed all fmt.Printf statements
* No more plaintext password logging
- Create safe context helper functions
* internal/handlers/context.go added
* GetAuthenticatedUser() for safe retrieval
* MustGetAuthenticatedUser() for post-auth middleware
Security: Critical
Tests: All 62 integration tests pass
Breaking: None - backward compatible
This commit is contained in:
+38
-96
@@ -186,22 +186,12 @@ func (h *AuthHandler) Register(c echo.Context) error {
|
||||
// Role-based restrictions: only admins can create admin users if any admin already exists
|
||||
if userRole == "admin" && adminExists {
|
||||
// Check if current user is admin (requires authentication)
|
||||
userID := c.Get("user_id")
|
||||
if userID == nil {
|
||||
// Not authenticated - cannot create admin user if admins exist
|
||||
user, ok := c.Get("user").(database.Users)
|
||||
if !ok || user.Role != "admin" {
|
||||
// Not authenticated or not admin - cannot create admin user if admins exist
|
||||
if c.Request().Header.Get("HX-Request") == "true" {
|
||||
return c.HTML(http.StatusForbidden, `<div class="text-red-500">Only existing administrators can create admin accounts</div>`)
|
||||
}
|
||||
return c.JSON(http.StatusForbidden, map[string]string{"error": "only existing administrators can create admin accounts"})
|
||||
}
|
||||
|
||||
// User is authenticated - check their role
|
||||
userRoleAuth := c.Get("user_role").(string)
|
||||
if userRoleAuth != "admin" {
|
||||
// Authenticated but not admin - cannot create admin accounts
|
||||
if c.Request().Header.Get("HX-Request") == "true" {
|
||||
return c.HTML(http.StatusForbidden, `<div class="text-red-500">Only administrators can create admin accounts</div>`)
|
||||
}
|
||||
return c.JSON(http.StatusForbidden, map[string]string{"error": "only administrators can create admin accounts"})
|
||||
}
|
||||
}
|
||||
@@ -285,27 +275,20 @@ window.location.href = '/bookshelf';
|
||||
|
||||
// Login handles POST /api/auth/login
|
||||
func (h *AuthHandler) Login(c echo.Context) error {
|
||||
// Debug logging
|
||||
fmt.Printf("Login request - Content-Type: %s\n", c.Request().Header.Get("Content-Type"))
|
||||
fmt.Printf("Form values - login: %s, password: %s\n", c.FormValue("login"), c.FormValue("password"))
|
||||
|
||||
// Try form data first (HTMX), then JSON (Bruno)
|
||||
login := c.FormValue("login")
|
||||
password := c.FormValue("password")
|
||||
|
||||
if login == "" || password == "" {
|
||||
fmt.Printf("Form values empty, trying JSON bind\n")
|
||||
// Fallback to JSON binding
|
||||
req := LoginRequest{}
|
||||
if err := c.Bind(&req); err != nil {
|
||||
fmt.Printf("JSON bind error: %v\n", err)
|
||||
if c.Request().Header.Get("HX-Request") == "true" {
|
||||
return c.HTML(http.StatusBadRequest, `<div class="text-red-500">Invalid request</div>`)
|
||||
}
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"})
|
||||
}
|
||||
if err := c.Validate(&req); err != nil {
|
||||
fmt.Printf("Validation error: %v\n", err)
|
||||
if c.Request().Header.Get("HX-Request") == "true" {
|
||||
return c.HTML(http.StatusBadRequest, `<div class="text-red-500">`+err.Error()+`</div>`)
|
||||
}
|
||||
@@ -313,12 +296,10 @@ func (h *AuthHandler) Login(c echo.Context) error {
|
||||
}
|
||||
login = req.Login
|
||||
password = req.Password
|
||||
fmt.Printf("JSON bind success - login: %s\n", login)
|
||||
}
|
||||
|
||||
req := LoginRequest{Login: login, Password: password}
|
||||
if err := c.Validate(&req); err != nil {
|
||||
fmt.Printf("Final validation error: %v\n", err)
|
||||
if c.Request().Header.Get("HX-Request") == "true" {
|
||||
return c.HTML(http.StatusBadRequest, `<div class="text-red-500">`+err.Error()+`</div>`)
|
||||
}
|
||||
@@ -443,19 +424,7 @@ window.location.href = '/bookshelf';
|
||||
|
||||
// GetProfile handles GET /api/auth/profile
|
||||
func (h *AuthHandler) GetProfile(c echo.Context) error {
|
||||
userID := c.Get("user_id").(string)
|
||||
userUUID, err := uuid.Parse(userID)
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user id"})
|
||||
}
|
||||
|
||||
user, err := h.db.GetUser(c.Request().Context(), pgtype.UUID{Bytes: userUUID, Valid: true})
|
||||
if err != nil {
|
||||
if err == pgx.ErrNoRows {
|
||||
return c.JSON(http.StatusNotFound, map[string]string{"error": "user not found"})
|
||||
}
|
||||
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
}
|
||||
user := MustGetAuthenticatedUser(c)
|
||||
|
||||
firstName := ""
|
||||
if user.FirstName.Valid {
|
||||
@@ -477,19 +446,15 @@ func (h *AuthHandler) GetProfile(c echo.Context) error {
|
||||
|
||||
// UpdateProfile handles PUT /api/auth/profile
|
||||
func (h *AuthHandler) UpdateProfile(c echo.Context) error {
|
||||
userID := c.Get("user_id").(string)
|
||||
userUUID, err := uuid.Parse(userID)
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user id"})
|
||||
}
|
||||
user := MustGetAuthenticatedUser(c)
|
||||
|
||||
var req UpdateProfileRequest
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"})
|
||||
}
|
||||
|
||||
err = h.db.UpdateUserProfile(c.Request().Context(), database.UpdateUserProfileParams{
|
||||
ID: pgtype.UUID{Bytes: userUUID, Valid: true},
|
||||
err := h.db.UpdateUserProfile(c.Request().Context(), database.UpdateUserProfileParams{
|
||||
ID: user.ID,
|
||||
FirstName: pgtype.Text{String: req.FirstName, Valid: req.FirstName != ""},
|
||||
LastName: pgtype.Text{String: req.LastName, Valid: req.LastName != ""},
|
||||
})
|
||||
@@ -618,11 +583,7 @@ type UpdateThemeRequest struct {
|
||||
|
||||
// UpdateTheme handles PUT /api/auth/theme
|
||||
func (h *AuthHandler) UpdateTheme(c echo.Context) error {
|
||||
userID := c.Get("user_id").(string)
|
||||
userUUID, err := uuid.Parse(userID)
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user id"})
|
||||
}
|
||||
user := MustGetAuthenticatedUser(c)
|
||||
|
||||
var req UpdateThemeRequest
|
||||
if err := c.Bind(&req); err != nil {
|
||||
@@ -632,8 +593,8 @@ func (h *AuthHandler) UpdateTheme(c echo.Context) error {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
}
|
||||
|
||||
err = h.db.UpdateUserTheme(c.Request().Context(), database.UpdateUserThemeParams{
|
||||
ID: pgtype.UUID{Bytes: userUUID, Valid: true},
|
||||
err := h.db.UpdateUserTheme(c.Request().Context(), database.UpdateUserThemeParams{
|
||||
ID: user.ID,
|
||||
Theme: pgtype.Text{String: req.Theme, Valid: req.Theme != ""},
|
||||
})
|
||||
if err != nil {
|
||||
@@ -649,11 +610,7 @@ type UpdateUsernameRequest struct {
|
||||
|
||||
// UpdateUsername handles PUT /api/auth/username
|
||||
func (h *AuthHandler) UpdateUsername(c echo.Context) error {
|
||||
userID := c.Get("user_id").(string)
|
||||
userUUID, err := uuid.Parse(userID)
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user id"})
|
||||
}
|
||||
user := MustGetAuthenticatedUser(c)
|
||||
|
||||
var req UpdateUsernameRequest
|
||||
if err := c.Bind(&req); err != nil {
|
||||
@@ -665,13 +622,13 @@ func (h *AuthHandler) UpdateUsername(c echo.Context) error {
|
||||
|
||||
// Check if username is already taken by another user
|
||||
existingUser, err := h.db.GetUserByUsername(c.Request().Context(), req.Username)
|
||||
if err == nil && uuid.UUID(existingUser.ID.Bytes) != userUUID {
|
||||
if err == nil && existingUser.ID.Bytes != user.ID.Bytes {
|
||||
return c.JSON(http.StatusConflict, map[string]string{"error": "username already taken"})
|
||||
}
|
||||
|
||||
// Update username
|
||||
err = h.db.UpdateUsername(c.Request().Context(), database.UpdateUsernameParams{
|
||||
ID: pgtype.UUID{Bytes: userUUID, Valid: true},
|
||||
ID: user.ID,
|
||||
Username: req.Username,
|
||||
})
|
||||
if err != nil {
|
||||
@@ -687,11 +644,7 @@ type UpdateEmailRequest struct {
|
||||
|
||||
// UpdateEmail handles PUT /api/auth/email
|
||||
func (h *AuthHandler) UpdateEmail(c echo.Context) error {
|
||||
userID := c.Get("user_id").(string)
|
||||
userUUID, err := uuid.Parse(userID)
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user id"})
|
||||
}
|
||||
user := MustGetAuthenticatedUser(c)
|
||||
|
||||
var req UpdateEmailRequest
|
||||
if err := c.Bind(&req); err != nil {
|
||||
@@ -703,13 +656,13 @@ func (h *AuthHandler) UpdateEmail(c echo.Context) error {
|
||||
|
||||
// Check if email is already taken by another user
|
||||
existingUser, err := h.db.GetUserByEmail(c.Request().Context(), req.Email)
|
||||
if err == nil && uuid.UUID(existingUser.ID.Bytes) != userUUID {
|
||||
if err == nil && existingUser.ID.Bytes != user.ID.Bytes {
|
||||
return c.JSON(http.StatusConflict, map[string]string{"error": "email already taken"})
|
||||
}
|
||||
|
||||
// Update email
|
||||
err = h.db.UpdateEmail(c.Request().Context(), database.UpdateEmailParams{
|
||||
ID: pgtype.UUID{Bytes: userUUID, Valid: true},
|
||||
ID: user.ID,
|
||||
Email: req.Email,
|
||||
})
|
||||
if err != nil {
|
||||
@@ -727,11 +680,7 @@ type UpdatePasswordRequest struct {
|
||||
|
||||
// UpdatePassword handles PUT /api/auth/password
|
||||
func (h *AuthHandler) UpdatePassword(c echo.Context) error {
|
||||
userID := c.Get("user_id").(string)
|
||||
userUUID, err := uuid.Parse(userID)
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user id"})
|
||||
}
|
||||
user := MustGetAuthenticatedUser(c)
|
||||
|
||||
var req UpdatePasswordRequest
|
||||
if err := c.Bind(&req); err != nil {
|
||||
@@ -747,7 +696,7 @@ func (h *AuthHandler) UpdatePassword(c echo.Context) error {
|
||||
}
|
||||
|
||||
// Get current user's password hash
|
||||
passwordHash, err := h.db.GetUserPasswordHash(c.Request().Context(), pgtype.UUID{Bytes: userUUID, Valid: true})
|
||||
passwordHash, err := h.db.GetUserPasswordHash(c.Request().Context(), user.ID)
|
||||
if err != nil {
|
||||
if err == pgx.ErrNoRows {
|
||||
return c.JSON(http.StatusNotFound, map[string]string{"error": "user not found"})
|
||||
@@ -768,7 +717,7 @@ func (h *AuthHandler) UpdatePassword(c echo.Context) error {
|
||||
|
||||
// Update password
|
||||
err = h.db.UpdatePassword(c.Request().Context(), database.UpdatePasswordParams{
|
||||
ID: pgtype.UUID{Bytes: userUUID, Valid: true},
|
||||
ID: user.ID,
|
||||
PasswordHash: string(hashedPassword),
|
||||
})
|
||||
if err != nil {
|
||||
@@ -781,23 +730,25 @@ func (h *AuthHandler) UpdatePassword(c echo.Context) error {
|
||||
// DeleteAccount handles DELETE /api/auth/account
|
||||
// Supports self-deletion or admin deletion of other users
|
||||
func (h *AuthHandler) DeleteAccount(c echo.Context) error {
|
||||
currentUser := MustGetAuthenticatedUser(c)
|
||||
|
||||
// Get target user ID from query parameter (for admin override) or use current user
|
||||
targetUserID := c.QueryParam("user_id")
|
||||
userID := c.Get("user_id").(string)
|
||||
var targetUserUUID pgtype.UUID
|
||||
|
||||
// If admin override is used, validate admin and use target
|
||||
if targetUserID != "" {
|
||||
// Admin override mode - check if current user is admin
|
||||
userRole := c.Get("user_role").(string)
|
||||
if userRole != "admin" {
|
||||
if currentUser.Role != "admin" {
|
||||
return c.JSON(http.StatusForbidden, map[string]string{"error": "admin access required"})
|
||||
}
|
||||
userID = targetUserID
|
||||
}
|
||||
|
||||
userUUID, err := uuid.Parse(userID)
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user id"})
|
||||
parsedUUID, err := uuid.Parse(targetUserID)
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user id"})
|
||||
}
|
||||
targetUserUUID = pgtype.UUID{Bytes: [16]byte(parsedUUID), Valid: true}
|
||||
} else {
|
||||
targetUserUUID = currentUser.ID
|
||||
}
|
||||
|
||||
// Check if this is the last admin user - prevent deletion
|
||||
@@ -814,8 +765,7 @@ func (h *AuthHandler) DeleteAccount(c echo.Context) error {
|
||||
adminCount++
|
||||
}
|
||||
// Find target user details
|
||||
userUUIDStr := uuid.UUID(user.ID.Bytes).String()
|
||||
if userUUIDStr == userID {
|
||||
if user.ID.Bytes == targetUserUUID.Bytes {
|
||||
targetUserRole = user.Role
|
||||
}
|
||||
}
|
||||
@@ -829,7 +779,7 @@ func (h *AuthHandler) DeleteAccount(c echo.Context) error {
|
||||
}
|
||||
|
||||
// Delete user (this will cascade to delete all related data)
|
||||
err = h.db.DeleteUser(c.Request().Context(), pgtype.UUID{Bytes: userUUID, Valid: true})
|
||||
err = h.db.DeleteUser(c.Request().Context(), targetUserUUID)
|
||||
if err != nil {
|
||||
if err == pgx.ErrNoRows {
|
||||
if c.Request().Header.Get("HX-Request") == "true" {
|
||||
@@ -845,7 +795,7 @@ func (h *AuthHandler) DeleteAccount(c echo.Context) error {
|
||||
|
||||
// Create success message based on context
|
||||
var message string
|
||||
if targetUserID != "" && userID != c.Get("user_id").(string) {
|
||||
if targetUserID != "" && targetUserUUID.Bytes != currentUser.ID.Bytes {
|
||||
message = "user account deleted successfully"
|
||||
} else {
|
||||
message = "account deleted successfully"
|
||||
@@ -861,11 +811,7 @@ type UpdateScanSettingsRequest struct {
|
||||
|
||||
// UpdateScanSettings handles PUT /api/library/scan-settings
|
||||
func (h *AuthHandler) UpdateScanSettings(c echo.Context) error {
|
||||
userID := c.Get("user_id").(string)
|
||||
userUUID, err := uuid.Parse(userID)
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user id"})
|
||||
}
|
||||
user := MustGetAuthenticatedUser(c)
|
||||
|
||||
var req UpdateScanSettingsRequest
|
||||
if err := c.Bind(&req); err != nil {
|
||||
@@ -875,8 +821,8 @@ func (h *AuthHandler) UpdateScanSettings(c echo.Context) error {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
}
|
||||
|
||||
err = h.db.UpdateScanSettings(c.Request().Context(), database.UpdateScanSettingsParams{
|
||||
ID: pgtype.UUID{Bytes: userUUID, Valid: true},
|
||||
err := h.db.UpdateScanSettings(c.Request().Context(), database.UpdateScanSettingsParams{
|
||||
ID: user.ID,
|
||||
ScanFrequencyMinutes: pgtype.Int4{Int32: req.ScanFrequencyMinutes, Valid: true},
|
||||
AutoScanEnabled: pgtype.Bool{Bool: req.AutoScanEnabled, Valid: true},
|
||||
})
|
||||
@@ -889,13 +835,9 @@ func (h *AuthHandler) UpdateScanSettings(c echo.Context) error {
|
||||
|
||||
// GetScanSettings handles GET /api/library/scan-settings
|
||||
func (h *AuthHandler) GetScanSettings(c echo.Context) error {
|
||||
userID := c.Get("user_id").(string)
|
||||
userUUID, err := uuid.Parse(userID)
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user id"})
|
||||
}
|
||||
user := MustGetAuthenticatedUser(c)
|
||||
|
||||
settings, err := h.db.GetScanSettings(c.Request().Context(), pgtype.UUID{Bytes: userUUID, Valid: true})
|
||||
settings, err := h.db.GetScanSettings(c.Request().Context(), user.ID)
|
||||
if err != nil {
|
||||
if err == pgx.ErrNoRows {
|
||||
// If no settings found, return defaults
|
||||
|
||||
Reference in New Issue
Block a user