fix(sec/d24): auth hardening — login lockout, rate limits, constant-time, trust proxy
Red-team highs/mediums (auth surface):
- staff_user gains failed_count/locked_until; login() locks an account for 15min
after 8 consecutive failures, clears the counter on success (both engines).
- login() is now constant-time: a missing email still spends one scrypt verify
against a fixed dummy hash — no timing/enumeration oracle. Returns a typed
{ok:false,reason} instead of null.
- POST /auth/login gets per-IP (20/min) AND per-email (8/15min) fixed-window
rate limiting layered over the lockout; 429 on trip.
- Credential reveal routes (db-password, module password, module secret) get a
per-user reveal limiter (20/min) so one insider cannot bulk-decrypt everything;
added missing getClientModule 404 guards.
- app.set(trust proxy,1) so req.ip is the real client behind nginx/Caddy and the
per-IP limiters actually bucket per client.
- makeRateLimiter extracted to rate-limit.ts (shared, no server<->api cycle).
Tests: lockout, counter-reset, unknown-email, and existing auth all green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
feat/client-detail-redesign
parent
08e3c02ae7
commit
10022965d1
@ -0,0 +1,21 @@
|
||||
/**
|
||||
* A dependency-free fixed-window rate limiter keyed by an opaque string (client IP,
|
||||
* account email, staff id, …). Returns true while the key is under `limit` in the
|
||||
* current `windowMs` window, false once it is exhausted. Expired buckets are swept
|
||||
* lazily so the map cannot grow without bound under key rotation. In its own module
|
||||
* so both the api router and the public-share route can share it without a cycle.
|
||||
*/
|
||||
export function makeRateLimiter(limit: number, windowMs: number): (key: string, now?: number) => boolean {
|
||||
const buckets = new Map<string, { count: number; resetAt: number }>()
|
||||
return (key: string, now = Date.now()): boolean => {
|
||||
if (buckets.size > 5000) for (const [k, b] of buckets) if (now >= b.resetAt) buckets.delete(k)
|
||||
const bucket = buckets.get(key)
|
||||
if (bucket === undefined || now >= bucket.resetAt) {
|
||||
buckets.set(key, { count: 1, resetAt: now + windowMs })
|
||||
return true
|
||||
}
|
||||
if (bucket.count >= limit) return false
|
||||
bucket.count += 1
|
||||
return true
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue