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

@@ -0,0 +1,5 @@
package image
type MetadataStripper interface {
Strip(contentType string, data []byte) ([]byte, error)
}
+12
View File
@@ -15,6 +15,8 @@ var ErrInviteAlreadyAccepted = errors.New("invite already accepted")
const Lifetime = 3 * 24 * time.Hour
const Retention = 30 * 24 * time.Hour
type Status string
const (
@@ -42,3 +44,13 @@ func (i Invite) StatusAt(now time.Time) Status {
}
return StatusExpired
}
func (i Invite) IsStaleAt(now time.Time) bool {
switch i.StatusAt(now) {
case StatusAccepted:
return now.After(i.AcceptedAt.Add(Retention))
case StatusExpired:
return now.After(i.ExpiresAt.Add(Retention))
}
return false
}
@@ -0,0 +1,11 @@
package retention
type Result struct {
RevokedSessions int
UnverifiedUsers int
Invites int
}
func (r Result) Total() int {
return r.RevokedSessions + r.UnverifiedUsers + r.Invites
}
+22
View File
@@ -0,0 +1,22 @@
package session
import (
"errors"
"time"
)
var ErrInvalidSessionID = errors.New("invalid session id")
const MaxIDLength = 64
type Revocation struct {
ID string
ExpiresAt time.Time
}
func ValidateID(id string) error {
if id == "" || len(id) > MaxIDLength {
return ErrInvalidSessionID
}
return nil
}
@@ -0,0 +1,9 @@
package session
import "time"
type Repository interface {
Revoke(revocation Revocation) error
IsRevoked(id string) (bool, error)
DeleteExpired(now time.Time) (int, error)
}
+26
View File
@@ -18,9 +18,19 @@ var ErrInvalidDisplayName = errors.New("invalid display name")
var ErrUsernameTaken = errors.New("username taken")
var ErrInvalidLookup = errors.New("invalid user lookup")
var ErrVerificationEmailNotSent = errors.New("verification email not sent")
var ErrPasswordTooLong = errors.New("password too long")
const MaxLookupIDs = 100
const MaxPasswordBytes = 72
func ValidatePassword(password string) error {
if len(password) > MaxPasswordBytes {
return ErrPasswordTooLong
}
return nil
}
type Role string
const (
@@ -45,6 +55,22 @@ type User struct {
UpdatedAt time.Time `json:"updated_at"`
}
const UnverifiedRetention = 7 * 24 * time.Hour
func (u User) IsStaleUnverifiedAt(now time.Time) bool {
if u.IsVerified {
return false
}
deadline := u.VerificationExpiration
if deadline.IsZero() {
deadline = u.CreatedAt
}
if deadline.IsZero() {
return false
}
return now.After(deadline.Add(UnverifiedRetention))
}
func (u User) NameOrUsername() string {
if trimmed := strings.TrimSpace(u.DisplayName); trimmed != "" {
return trimmed