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
+34
-12
@@ -14,8 +14,19 @@ type rateLimitWindow struct {
|
||||
count int
|
||||
}
|
||||
|
||||
type rateLimitCounters struct {
|
||||
mu sync.Mutex
|
||||
window time.Duration
|
||||
windows map[string]*rateLimitWindow
|
||||
lastSweep time.Time
|
||||
}
|
||||
|
||||
func NewRateLimiter(maxAttempts int, window time.Duration) func(http.Handler) http.Handler {
|
||||
return newKeyedRateLimiter(maxAttempts, window, func(r *http.Request) string { return r.RemoteAddr })
|
||||
return newKeyedRateLimiter(maxAttempts, window, connectionHostKey)
|
||||
}
|
||||
|
||||
func connectionHostKey(r *http.Request) string {
|
||||
return hostOf(r.RemoteAddr)
|
||||
}
|
||||
|
||||
func NewUserRateLimiter(maxAttempts int, window time.Duration) func(http.Handler) http.Handler {
|
||||
@@ -28,12 +39,11 @@ func userRateLimitKey(r *http.Request) string {
|
||||
}
|
||||
|
||||
func newKeyedRateLimiter(maxAttempts int, window time.Duration, keyOf func(*http.Request) string) func(http.Handler) http.Handler {
|
||||
var mu sync.Mutex
|
||||
windows := make(map[string]*rateLimitWindow)
|
||||
counters := &rateLimitCounters{window: window, windows: make(map[string]*rateLimitWindow)}
|
||||
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
retryAfter, allowed := recordAttempt(&mu, windows, keyOf(r), maxAttempts, window)
|
||||
retryAfter, allowed := counters.recordAttempt(keyOf(r), maxAttempts, time.Now())
|
||||
if !allowed {
|
||||
w.Header().Set("Retry-After", strconv.Itoa(retryAfter))
|
||||
http.Error(w, apperr.TooManyRequestsErrorMessage, http.StatusTooManyRequests)
|
||||
@@ -44,24 +54,36 @@ func newKeyedRateLimiter(maxAttempts int, window time.Duration, keyOf func(*http
|
||||
}
|
||||
}
|
||||
|
||||
func recordAttempt(mu *sync.Mutex, windows map[string]*rateLimitWindow, key string, maxAttempts int, window time.Duration) (int, bool) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
func (c *rateLimitCounters) recordAttempt(key string, maxAttempts int, now time.Time) (int, bool) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
current, exists := windows[key]
|
||||
if !exists || now.Sub(current.start) >= window {
|
||||
c.sweepExpired(now)
|
||||
current, exists := c.windows[key]
|
||||
if !exists || now.Sub(current.start) >= c.window {
|
||||
current = &rateLimitWindow{start: now}
|
||||
windows[key] = current
|
||||
c.windows[key] = current
|
||||
}
|
||||
|
||||
current.count++
|
||||
if current.count > maxAttempts {
|
||||
return retryAfterSeconds(window - now.Sub(current.start)), false
|
||||
return retryAfterSeconds(c.window - now.Sub(current.start)), false
|
||||
}
|
||||
return 0, true
|
||||
}
|
||||
|
||||
func (c *rateLimitCounters) sweepExpired(now time.Time) {
|
||||
if now.Sub(c.lastSweep) < c.window {
|
||||
return
|
||||
}
|
||||
c.lastSweep = now
|
||||
for key, current := range c.windows {
|
||||
if now.Sub(current.start) >= c.window {
|
||||
delete(c.windows, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func retryAfterSeconds(remaining time.Duration) int {
|
||||
seconds := int(remaining / time.Second)
|
||||
if remaining%time.Second > 0 {
|
||||
|
||||
Reference in new issue
Block a user