auth: reset links pause for a day after five (audit A3)
Security audit 2026-10-17, finding A3 (low), stage 3. sendReset spaced reset mails ten minutes apart and capped them at five an hour per account, so a stranger who repeated a username kept a live 30-minute link arriving in the administrator's mailbox five times an hour, for ever. Now five reset links a day per account (resetDayLimiter); the sixth request sends one notice instead - "Reset links paused for a day", no link, saying further requests are ignored for 24 hours and that a global administrator can set a password on Server > Users - and every later one in the window sends nothing (resetNoticeLimiter), logged either way. In memory, like every limiter: a restart clears them. Test: TestResetLinksPauseForADayAfterFive. Docs: security.md, guide § Account; CHANGELOG; plan. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -49,6 +49,11 @@ type Module struct {
|
||||
linkLimiter *rateLimiter
|
||||
linkAddrLimiter *rateLimiter
|
||||
mailLimiter *rateLimiter
|
||||
// resetDayLimiter counts the reset links mailed to one account in a
|
||||
// day, resetNoticeLimiter the one notice that further requests are
|
||||
// ignored (links.go, sendReset).
|
||||
resetDayLimiter *rateLimiter
|
||||
resetNoticeLimiter *rateLimiter
|
||||
|
||||
proxyWatch proxyWatch
|
||||
refusals *refusalLog
|
||||
@@ -88,8 +93,12 @@ func New(st *store.Store, cfg Config, v *view.Engine, setupTokenPath string) *Mo
|
||||
// Five links an hour to one address, of either kind: three resets
|
||||
// asked for and a couple of confirmations is ordinary use, a mailbox
|
||||
// full of them is not.
|
||||
mailLimiter: newRateLimiter(5, time.Hour),
|
||||
trustedProxies: cfg.TrustedProxyCIDRs,
|
||||
mailLimiter: newRateLimiter(5, time.Hour),
|
||||
// Five reset links a day to one account, then one notice and
|
||||
// silence for the rest of the day (security audit 2026-10-17, A3).
|
||||
resetDayLimiter: newRateLimiter(resetsPerDay, 24*time.Hour),
|
||||
resetNoticeLimiter: newRateLimiter(1, 24*time.Hour),
|
||||
trustedProxies: cfg.TrustedProxyCIDRs,
|
||||
}
|
||||
m.setupLimiter.startSweeper()
|
||||
m.loginLimiter.startSweeper()
|
||||
@@ -101,6 +110,8 @@ func New(st *store.Store, cfg Config, v *view.Engine, setupTokenPath string) *Mo
|
||||
m.resetLimiter.startSweeper()
|
||||
m.linkLimiter.startSweeper()
|
||||
m.mailLimiter.startSweeper()
|
||||
m.resetDayLimiter.startSweeper()
|
||||
m.resetNoticeLimiter.startSweeper()
|
||||
// Made now rather than at the first unknown name, which would otherwise
|
||||
// pay for two bcrypt runs (review of audit stage 4).
|
||||
go noSuchUserHash()
|
||||
|
||||
Reference in New Issue
Block a user