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

@ -15,16 +15,18 @@ import (
const MaxLoginAttempts = 5
type AuthHandler struct {
db *db.DB
jwtManager *auth.JWTManager
emailService *mail.EmailService
db *db.DB
jwtManager *auth.JWTManager
emailService *mail.EmailService
trustedProxies []string
}
func NewAuthHandler(database *db.DB, jwtManager *auth.JWTManager, emailService *mail.EmailService) *AuthHandler {
func NewAuthHandler(database *db.DB, jwtManager *auth.JWTManager, emailService *mail.EmailService, trustedProxies []string) *AuthHandler {
return &AuthHandler{
db: database,
jwtManager: jwtManager,
emailService: emailService,
db: database,
jwtManager: jwtManager,
emailService: emailService,
trustedProxies: trustedProxies,
}
}
@ -56,7 +58,9 @@ func (h *AuthHandler) Login(c *gin.Context) {
return
}
ip := getClientIP(c)
h.cleanupOldAttempts()
ip := h.getClientIP(c)
if isLockedOut(ip, req.Username, h.db) {
Error(c, http.StatusTooManyRequests, "too many failed attempts, try again later")
@ -241,10 +245,24 @@ func clearFailedAttempts(identifier, ip string, database *db.DB) {
Update("successful", true)
}
func getClientIP(c *gin.Context) string {
forwarded := c.GetHeader("X-Forwarded-For")
if forwarded != "" {
return strings.Split(forwarded, ",")[0]
}
return c.ClientIP()
func (h *AuthHandler) cleanupOldAttempts() {
cutoff := time.Now().Add(-24 * time.Hour)
h.db.Where("attempted_at < ? AND successful = false", cutoff).Delete(&db.ImcLoginAttempt{})
}
func (h *AuthHandler) getClientIP(c *gin.Context) string {
remoteIP := c.ClientIP()
if len(h.trustedProxies) > 0 {
for _, proxy := range h.trustedProxies {
if remoteIP == proxy {
forwarded := c.GetHeader("X-Forwarded-For")
if forwarded != "" {
return strings.Split(forwarded, ",")[0]
}
}
}
}
return remoteIP
}

View file

@ -27,7 +27,7 @@ func New(database *db.DB, cfg *config.Config) *Router {
emailService := mail.NewEmailService(cfg)
r := &Router{
authHandler: handlers.NewAuthHandler(database, jwtManager, emailService),
authHandler: handlers.NewAuthHandler(database, jwtManager, emailService, cfg.TrustedProxies),
domainHandler: handlers.NewDomainHandler(database),
userHandler: handlers.NewUserHandler(database),
aliasHandler: handlers.NewAliasHandler(database),