From 763aea15cb3b2d3babb4f541ebd922bf666104f4 Mon Sep 17 00:00:00 2001 From: senke Date: Sun, 22 Feb 2026 17:36:10 +0100 Subject: [PATCH] fix(security): hash password reset tokens before database storage INF-10: Reset tokens are now SHA-256 hashed before INSERT. Validation hashes the received token and compares against stored hash. Plain tokens never persisted. --- .../internal/services/password_service.go | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/veza-backend-api/internal/services/password_service.go b/veza-backend-api/internal/services/password_service.go index d12cb66b0..c5d8d02a8 100644 --- a/veza-backend-api/internal/services/password_service.go +++ b/veza-backend-api/internal/services/password_service.go @@ -3,8 +3,10 @@ package services import ( "context" "crypto/rand" + "crypto/sha256" "database/sql" "encoding/base64" + "encoding/hex" "fmt" "strings" "time" @@ -53,6 +55,13 @@ func NewPasswordService(db *database.Database, logger *zap.Logger) *PasswordServ } } +// hashToken returns the hex-encoded SHA-256 hash of the token. +// INF-10: Tokens are stored hashed in the DB; only the hash is persisted. +func (ps *PasswordService) hashToken(token string) string { + h := sha256.Sum256([]byte(token)) + return hex.EncodeToString(h[:]) +} + // GetUserByEmail retrieves a user by email func (ps *PasswordService) GetUserByEmail(email string) (*UserInfo, error) { ctx := context.Background() @@ -87,12 +96,13 @@ func (ps *PasswordService) GeneratePasswordResetToken(userID uuid.UUID) (string, // Set expiration (1 hour) expiresAt := time.Now().Add(1 * time.Hour) - // Store in database + // INF-10: Store hash in DB, return plain token to user ctx := context.Background() + tokenHash := ps.hashToken(token) _, err = ps.db.ExecContext(ctx, ` INSERT INTO password_reset_tokens (user_id, token, expires_at, used) VALUES ($1, $2, $3, FALSE) - `, userID, token, expiresAt) + `, userID, tokenHash, expiresAt) if err != nil { return "", time.Time{}, err @@ -109,13 +119,14 @@ func (ps *PasswordService) GeneratePasswordResetToken(userID uuid.UUID) (string, func (ps *PasswordService) ResetPassword(token, newPassword string) error { ctx := context.Background() - // Get token info + // INF-10: Hash received token and look up by hash + tokenHash := ps.hashToken(token) var resetToken PasswordResetToken err := ps.db.QueryRowContext(ctx, ` SELECT id, user_id, token, expires_at, used, created_at FROM password_reset_tokens WHERE token = $1 AND used = FALSE - `, token).Scan( + `, tokenHash).Scan( &resetToken.ID, &resetToken.UserID, &resetToken.Token,