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
@@ -66,6 +66,10 @@ func (a *AcceptInviteUseCase) accept(command AcceptInviteCommand, pending invite
|
||||
return user.User{}, err
|
||||
}
|
||||
|
||||
if err := user.ValidatePassword(command.Password); err != nil {
|
||||
return user.User{}, err
|
||||
}
|
||||
|
||||
hashedPassword, err := a.hasher.Hash(command.Password)
|
||||
if err != nil {
|
||||
return user.User{}, err
|
||||
|
||||
@@ -5,17 +5,21 @@ import (
|
||||
"RenewCMS/internal/domain/hash"
|
||||
"RenewCMS/internal/domain/user"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type AuthenticateUserUseCase struct {
|
||||
userRepository user.Repository
|
||||
hasher hash.Hasher
|
||||
recorder audit.Recorder
|
||||
decoyOnce sync.Once
|
||||
decoyHash string
|
||||
}
|
||||
|
||||
func NewAuthenticateUserUseCase(userRepository user.Repository, hasher hash.Hasher, recorder audit.Recorder) *AuthenticateUserUseCase {
|
||||
return &AuthenticateUserUseCase{userRepository, hasher, recorder}
|
||||
return &AuthenticateUserUseCase{userRepository: userRepository, hasher: hasher, recorder: recorder}
|
||||
}
|
||||
|
||||
func (a *AuthenticateUserUseCase) Authenticate(ctx context.Context, username string, password string) (user.User, error) {
|
||||
@@ -34,6 +38,9 @@ func (a *AuthenticateUserUseCase) Authenticate(ctx context.Context, username str
|
||||
func (a *AuthenticateUserUseCase) verifyCredentials(username string, password string) (user.User, error) {
|
||||
dbUser, err := a.userRepository.GetByUsername(username)
|
||||
if err != nil {
|
||||
if errors.Is(err, user.ErrUserNotFound) {
|
||||
a.compareWithDecoy(password)
|
||||
}
|
||||
return user.User{}, err
|
||||
}
|
||||
|
||||
@@ -44,6 +51,15 @@ func (a *AuthenticateUserUseCase) verifyCredentials(username string, password st
|
||||
return dbUser, nil
|
||||
}
|
||||
|
||||
func (a *AuthenticateUserUseCase) compareWithDecoy(password string) {
|
||||
a.decoyOnce.Do(func() {
|
||||
a.decoyHash, _ = a.hasher.Hash(rand.Text())
|
||||
})
|
||||
if a.decoyHash != "" {
|
||||
_ = a.hasher.Compare(a.decoyHash, password)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AuthenticateUserUseCase) recordRejectedLogin(ctx context.Context, username string, cause error) {
|
||||
if !errors.Is(cause, user.ErrUserNotFound) && !errors.Is(cause, user.ErrInvalidCredentials) {
|
||||
return
|
||||
|
||||
@@ -29,10 +29,11 @@ type CreateImageUseCase struct {
|
||||
imageRepository image.Repository
|
||||
storage image.Storage
|
||||
recorder audit.Recorder
|
||||
stripper image.MetadataStripper
|
||||
}
|
||||
|
||||
func NewCreateImageUseCase(imageRepository image.Repository, storage image.Storage, recorder audit.Recorder) *CreateImageUseCase {
|
||||
return &CreateImageUseCase{imageRepository, storage, recorder}
|
||||
func NewCreateImageUseCase(imageRepository image.Repository, storage image.Storage, recorder audit.Recorder, stripper image.MetadataStripper) *CreateImageUseCase {
|
||||
return &CreateImageUseCase{imageRepository, storage, recorder, stripper}
|
||||
}
|
||||
|
||||
func (g *CreateImageUseCase) CreateImage(ctx context.Context, input image.ImageInput) (image.Image, error) {
|
||||
@@ -74,6 +75,11 @@ func (g *CreateImageUseCase) create(input image.ImageInput) (image.Image, error)
|
||||
return image.Image{}, err
|
||||
}
|
||||
|
||||
data, err = g.stripper.Strip(input.ContentType, data)
|
||||
if err != nil {
|
||||
return image.Image{}, err
|
||||
}
|
||||
|
||||
checksum := sha256.Sum256(data)
|
||||
storedName, err := g.storage.Put(input.ContentType, bytes.NewReader(data))
|
||||
if err != nil {
|
||||
|
||||
@@ -55,6 +55,10 @@ func (g *CreateUserUseCase) create(createUser CreateUserCommand) (user.User, err
|
||||
return user.User{}, user.ErrRegistrationClosed
|
||||
}
|
||||
|
||||
if err := user.ValidatePassword(createUser.Password); err != nil {
|
||||
return user.User{}, err
|
||||
}
|
||||
|
||||
rawUuid := uuid.NewString()
|
||||
|
||||
hashedPassword, err := g.hasher.Hash(createUser.Password)
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package useCases
|
||||
|
||||
import (
|
||||
"RenewCMS/internal/domain/invite"
|
||||
"RenewCMS/internal/domain/retention"
|
||||
"RenewCMS/internal/domain/session"
|
||||
"RenewCMS/internal/domain/user"
|
||||
"time"
|
||||
)
|
||||
|
||||
type EnforceRetentionUseCase struct {
|
||||
users user.Repository
|
||||
invites invite.Repository
|
||||
sessions session.Repository
|
||||
}
|
||||
|
||||
func NewEnforceRetentionUseCase(users user.Repository, invites invite.Repository, sessions session.Repository) *EnforceRetentionUseCase {
|
||||
return &EnforceRetentionUseCase{users, invites, sessions}
|
||||
}
|
||||
|
||||
func (e *EnforceRetentionUseCase) Tick(now time.Time) (retention.Result, error) {
|
||||
var result retention.Result
|
||||
var err error
|
||||
|
||||
if result.RevokedSessions, err = e.sessions.DeleteExpired(now); err != nil {
|
||||
return result, err
|
||||
}
|
||||
if result.UnverifiedUsers, err = e.deleteStaleUnverifiedUsers(now); err != nil {
|
||||
return result, err
|
||||
}
|
||||
result.Invites, err = e.deleteStaleInvites(now)
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (e *EnforceRetentionUseCase) deleteStaleUnverifiedUsers(now time.Time) (int, error) {
|
||||
removed := 0
|
||||
for _, candidate := range e.users.GetAll() {
|
||||
if !candidate.IsStaleUnverifiedAt(now) {
|
||||
continue
|
||||
}
|
||||
if err := e.users.Delete(candidate.ID); err != nil {
|
||||
return removed, err
|
||||
}
|
||||
removed++
|
||||
}
|
||||
return removed, nil
|
||||
}
|
||||
|
||||
func (e *EnforceRetentionUseCase) deleteStaleInvites(now time.Time) (int, error) {
|
||||
removed := 0
|
||||
for _, candidate := range e.invites.GetAll() {
|
||||
if !candidate.IsStaleAt(now) {
|
||||
continue
|
||||
}
|
||||
if err := e.invites.Delete(candidate.ID); err != nil {
|
||||
return removed, err
|
||||
}
|
||||
removed++
|
||||
}
|
||||
return removed, nil
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
const exportBatchSize = 500
|
||||
|
||||
const formulaGuard = "'"
|
||||
const formulaStarts = "=+-@"
|
||||
const formulaStarts = "=+-@\t\r"
|
||||
|
||||
var exportHeader = []string{"when", "actor", "action", "target", "outcome", "source", "ip"}
|
||||
|
||||
|
||||
@@ -35,6 +35,10 @@ func (r *ResetPasswordUseCase) ResetPassword(ctx context.Context, email string,
|
||||
return user.ErrPasswordResetCodeExpired
|
||||
}
|
||||
|
||||
if err := user.ValidatePassword(newPassword); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
hashedPassword, err := r.hasher.Hash(newPassword)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package useCases
|
||||
|
||||
import (
|
||||
"RenewCMS/internal/domain/session"
|
||||
"time"
|
||||
)
|
||||
|
||||
type SessionRevocationUseCase struct {
|
||||
sessions session.Repository
|
||||
now func() time.Time
|
||||
}
|
||||
|
||||
func NewSessionRevocationUseCase(sessions session.Repository, now func() time.Time) *SessionRevocationUseCase {
|
||||
return &SessionRevocationUseCase{sessions, now}
|
||||
}
|
||||
|
||||
func (s *SessionRevocationUseCase) Revoke(id string, expiresAt time.Time) error {
|
||||
if err := session.ValidateID(id); err != nil {
|
||||
return err
|
||||
}
|
||||
if !expiresAt.After(s.now()) {
|
||||
return nil
|
||||
}
|
||||
return s.sessions.Revoke(session.Revocation{ID: id, ExpiresAt: expiresAt})
|
||||
}
|
||||
|
||||
func (s *SessionRevocationUseCase) IsRevoked(id string) (bool, error) {
|
||||
if err := session.ValidateID(id); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return s.sessions.IsRevoked(id)
|
||||
}
|
||||
@@ -44,6 +44,7 @@ var failureReasons = []failureReason{
|
||||
{user.ErrVerificationCodeExpired, "The verification code has expired."},
|
||||
{user.ErrInvalidDisplayName, "That display name is too long."},
|
||||
{user.ErrUsernameTaken, "That username is already taken."},
|
||||
{user.ErrPasswordTooLong, "The password is longer than 72 bytes."},
|
||||
{invite.ErrEmailHasAccount, "That email already has an account."},
|
||||
{invite.ErrPendingInvite, "An invitation is already pending for that email."},
|
||||
{invite.ErrInviteEmailNotSent, "The invitation email could not be sent."},
|
||||
|
||||
Reference in new issue
Block a user