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:
1 parent
e2812ee0b9
commit
ea98c5ec05
118 files changed
+3828
-130
No files matched your search
@@ -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")
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user