fix: security & privacy (rate limiter fix, csrf session cookie, logout on server, drafts stale token passw, login timing, password over 72b, JWT_SECRET complexity, SMTP TLS, CSV export neutralised, fonts bundled, upload metadata, hourly cleanup job, privacy.md report)

This commit is contained in:
Florian Sylvain committed 2026-10-22 00:49:19 +02:00
1 parent e2812ee0b9
commit ea98c5ec05
118 files changed
+3828 -130

No files matched your search

+96
View File
@@ -0,0 +1,96 @@
package test
import (
"RenewCMS/api/pkg/apperr"
validator "RenewCMS/api/pkg/validator"
"RenewCMS/internal/domain/user"
"errors"
"net/http"
"strings"
"testing"
"time"
)
func TestPasswordLengthLimit(t *testing.T) {
validator.InitValidator()
longest := strings.Repeat("p", user.MaxPasswordBytes)
tooLong := longest + "p"
multibyteTooLong := strings.Repeat("é", 37)
t.Run("The domain accepts 72 bytes and refuses 73, counting bytes rather than characters", func(t *testing.T) {
if err := user.ValidatePassword(longest); err != nil {
t.Errorf("72 bytes: expected no error, got %v", err)
}
if err := user.ValidatePassword(tooLong); !errors.Is(err, user.ErrPasswordTooLong) {
t.Errorf("73 bytes: expected ErrPasswordTooLong, got %v", err)
}
if err := user.ValidatePassword(multibyteTooLong); !errors.Is(err, user.ErrPasswordTooLong) {
t.Errorf("37 two-byte characters: expected ErrPasswordTooLong, got %v", err)
}
})
t.Run("Registering with a password over 72 bytes answers 400 and creates nothing", func(t *testing.T) {
userRepo := newMockUserRepo()
mailRepo := newMockMailRepo()
router := newRegistrationGateRouter(userRepo, mailRepo, "https://cms.example.com")
rec := registerRequest(t, router, "alice", multibyteTooLong, "alice@example.com")
if rec.Code != http.StatusBadRequest || strings.TrimSpace(rec.Body.String()) != apperr.PasswordTooLongErrorMessage {
t.Errorf("expected 400 %q, got %d %q", apperr.PasswordTooLongErrorMessage, rec.Code, rec.Body.String())
}
if len(userRepo.users) != 0 || len(mailRepo.SentEmails) != 0 {
t.Errorf("expected no user and no mail, got %d users and %d mails", len(userRepo.users), len(mailRepo.SentEmails))
}
})
t.Run("Registering with exactly 72 bytes still works", func(t *testing.T) {
userRepo := newMockUserRepo()
router := newRegistrationGateRouter(userRepo, newMockMailRepo(), "https://cms.example.com")
if rec := registerRequest(t, router, "alice", longest, "alice@example.com"); rec.Code != http.StatusOK {
t.Errorf("expected 200, got %d: %s", rec.Code, rec.Body.String())
}
})
t.Run("Resetting to a password over 72 bytes answers 400 and keeps the old password and code", func(t *testing.T) {
userRepo := newAliceResetPasswordRepo(time.Now().Add(time.Hour))
router := newResetPasswordRouter(userRepo, &stubHasher{})
rec := sendResetPasswordRequest(router, `{"email": "alice@example.com", "code": "reset-code-1", "password": "`+tooLong+`"}`)
if rec.Code != http.StatusBadRequest || strings.TrimSpace(rec.Body.String()) != apperr.PasswordTooLongErrorMessage {
t.Errorf("expected 400 %q, got %d %q", apperr.PasswordTooLongErrorMessage, rec.Code, rec.Body.String())
}
stored := userRepo.users[1]
if stored.Password != "hashed:oldpassword" || stored.PasswordResetCode != "hashed:reset-code-1" {
t.Errorf("expected the account untouched, got %+v", stored)
}
})
t.Run("A wrong reset code is reported as such even with an over-long password", func(t *testing.T) {
userRepo := newAliceResetPasswordRepo(time.Now().Add(time.Hour))
router := newResetPasswordRouter(userRepo, &stubHasher{})
rec := sendResetPasswordRequest(router, `{"email": "alice@example.com", "code": "wrong", "password": "`+tooLong+`"}`)
if rec.Code != http.StatusForbidden {
t.Errorf("expected 403, got %d", rec.Code)
}
})
t.Run("Accepting an invite with a password over 72 bytes answers 400 and creates nothing", func(t *testing.T) {
f := newAcceptInviteFixture(t, 100)
f.seedPending()
usersBefore := len(f.users.users)
rec := f.accept(acceptBody(acceptCode, "newadmin", tooLong))
if rec.Code != http.StatusBadRequest || strings.TrimSpace(rec.Body.String()) != apperr.PasswordTooLongErrorMessage {
t.Errorf("expected 400 %q, got %d %q", apperr.PasswordTooLongErrorMessage, rec.Code, rec.Body.String())
}
if len(f.users.users) != usersBefore {
t.Errorf("expected no user created")
}
})
}