Security fixes

1. Password reset tokens: use crypto/rand instead of math/rand
2. IP spoofing: only trust X-Forwarded-For from configured trusted proxies
3. Cleanup: purge failed login attempts older than 24 hours
4. JWT secret: validate minimum 32 character length on startup

Updated .env.example with TRUSTED_PROXIES setting
This commit is contained in:
Christoph Haas 2026-03-23 01:09:26 +01:00
parent ccaa1eca7e
commit 269fb6f53a
6 changed files with 82 additions and 46 deletions

View file

@ -3,10 +3,11 @@
package mail
import (
"crypto/tls" // TLS/SSL encryption for secure email sending
"fmt" // string formatting for URLs and messages
"net/smtp" // simple mail transfer protocol package
"strings" // string manipulation for token validation
"crypto/rand" // cryptographically secure random number generator
"crypto/tls" // TLS/SSL encryption for secure email sending
"encoding/hex" // hex encoding for token generation
"fmt" // string formatting for URLs and messages
"net/smtp" // simple mail transfer protocol package
"github.com/imc-vibe/backend/internal/config"
)
@ -166,40 +167,24 @@ func (s *EmailService) sendWithTLS(addr, to, msg string) error {
return client.Quit()
}
// GenerateToken creates a random token for password reset links.
// GenerateToken creates a cryptographically random token for password reset links.
// Returns a 64-character hexadecimal string (32 bytes encoded as hex).
// This is cryptographically random and suitable for security-sensitive tokens.
// Uses crypto/rand for security-sensitive token generation.
func GenerateToken() string {
// Create a byte slice to hold random bytes.
b := make([]byte, 32)
// Fill with random characters from our allowed set.
// We iterate over the slice and pick characters based on position.
for i := range b {
b[i] = tokenChars[i%len(tokenChars)]
if _, err := rand.Read(b); err != nil {
panic("crypto/rand failed: " + err.Error())
}
// Convert bytes to hex string. Each byte becomes two hex characters.
// Result: 32 bytes = 64 hex characters.
return fmt.Sprintf("%x", b)
return hex.EncodeToString(b)
}
// tokenChars defines the character set used for generating reset tokens.
// Using alphanumeric characters makes tokens easier to copy/paste.
const tokenChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
// ValidateToken checks if a token has the correct format.
// Returns true if the token is exactly 64 characters and contains only
// characters from our allowed tokenChars set.
// Returns true if the token is exactly 64 characters of valid hex.
// This is a quick validation before checking the database.
func ValidateToken(token string) bool {
// Tokens should be 64 hex characters (32 bytes * 2 hex chars/byte).
if len(token) != 64 {
return false
}
// Check each character is in our allowed set.
for _, c := range token {
if !strings.ContainsRune(tokenChars, c) {
return false
}
}
return true
_, err := hex.DecodeString(token)
return err == nil
}