Address code review findings across API, web and deployment
release gate / check (push) Failing after 6m43s
release gate / check (push) Failing after 6m43s
High severity: - wire TRUSTED_PROXY through Compose/deploy/docs and warn at web startup - bucket public rate limits on effective client IP, add per-bookmark budget, and forward the signed browser identity from the share page - disable first-run bootstrap in server deployments and document seeding - serialize last-admin demotion in one advisory-locked transaction Medium/low: - cap and batch share recipient inserts - add a shared outbound budget for preview/CardDAV/IMAP/signal work - pin CardDAV dials to 443 and bound IMAP logout - fix rightward kanban column drags and normalize cards on column delete - classify backend outages in auth guards, add request timeouts and an error boundary, remove the dead login route, surface throttling - enforce pinned/unpinned page reorder grouping server-side - avoid preview work for stale bookmark versions; drop duplicate dialog fetch - sequence-guard collection/tag/calendar/quick-find loads - validate property config and fix MULTI_SELECT/DATE filter inputs - document index maintenance; apply same-origin check to sign-out - trim signup names, unique shared dates and kanban keys (migration 010), reset reference cache on identity changes, hide OIDC issuer, return 413 for oversized direct bodies, escape Markdown link destinations - remove dead template files and duplicate test helper Verified with go build/vet, full PostgreSQL race suite, db convergence, tsc, both ESLint configs and a Next production build.
This commit is contained in:
+21
-15
@@ -35,13 +35,13 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
loginIPLimit = 20 // logins per IP per window
|
||||
loginIPWindow = time.Minute //
|
||||
signupIPLimit = 10 // signups per IP per window
|
||||
signupIPWindow = time.Hour //
|
||||
failureThreshold = 5 // consecutive failures before lockout
|
||||
failureLockout = 15 * time.Minute
|
||||
failureWindow = time.Hour // failures older than this are forgotten
|
||||
loginIPLimit = 20 // logins per IP per window
|
||||
loginIPWindow = time.Minute //
|
||||
signupIPLimit = 10 // signups per IP per window
|
||||
signupIPWindow = time.Hour //
|
||||
failureThreshold = 5 // consecutive failures before lockout
|
||||
failureLockout = 15 * time.Minute
|
||||
failureWindow = time.Hour // failures older than this are forgotten
|
||||
|
||||
// limiterMaxEntries is the soft cap per store. Review SEC-03: the failure
|
||||
// tracker previously never evicted sub-threshold entries (zero until), so
|
||||
@@ -169,9 +169,6 @@ func ResetRateLimits() {
|
||||
}
|
||||
}
|
||||
|
||||
// resetRateLimits clears all limiter state. Tests only.
|
||||
func resetRateLimits() { ResetRateLimits() }
|
||||
|
||||
func clientIP(r *http.Request) string {
|
||||
host, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
@@ -285,14 +282,23 @@ func RateLimitSignup(next http.HandlerFunc) http.HandlerFunc {
|
||||
// callers (e.g. public endpoints) can have independent buckets.
|
||||
func NewLimiterStore() *limiterStore { return newLimiterStore() }
|
||||
|
||||
// Limiter wraps a handler with a simple per-IP fixed-window rate limit.
|
||||
// Uses the direct peer IP (no forwarded headers) and does not exempt
|
||||
// loopback — public endpoints apply to every peer.
|
||||
// Limiter wraps a handler with a simple per-client fixed-window rate limit.
|
||||
// Review H-2: this buckets on EffectiveClientIP (the signed forwarded browser
|
||||
// identity when the trusted Next server attested one), not the raw direct
|
||||
// peer. Behind the Next proxy every public request otherwise shared one
|
||||
// instance-wide bucket, so a single caller could deny the endpoint to all
|
||||
// users. Loopback is not exempt — public endpoints apply to every peer.
|
||||
func Limiter(s *limiterStore, limit int, window time.Duration) func(http.HandlerFunc) http.HandlerFunc {
|
||||
return LimiterKeyed(s, limit, window, EffectiveClientIP)
|
||||
}
|
||||
|
||||
// LimiterKeyed is Limiter with a caller-supplied bucket key. It backs the
|
||||
// second public-share layer (review H-2): one hot bookmark id gets its own
|
||||
// generous budget in addition to the per-client one.
|
||||
func LimiterKeyed(s *limiterStore, limit int, window time.Duration, key func(*http.Request) string) func(http.HandlerFunc) http.HandlerFunc {
|
||||
return func(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ip := clientIP(r)
|
||||
if ok, retry := s.allow(ip, limit, window, time.Now()); !ok {
|
||||
if ok, retry := s.allow(key(r), limit, window, time.Now()); !ok {
|
||||
tooManyRequests(w, retry)
|
||||
return
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user