Security fixes from pentest 2026-10-09 (F1, F2, F4, F7)
- F2: per-IP rate limits on POST /api/users, /login, /forgot-password via new Next middleware (5/h signup, 10/15min login, 5/h forgot); 429 + Retry-After, generic Arabic error, no UX change - F7: ghost-email logins burn the same PBKDF2 cost (600k/32/sha256) as real verifies, closing response-time user enumeration - F4: HSTS max-age=63072000; includeSubDomains on all routes - F1: Traefik pagelimit (60/min, burst 120) + connlimit (in-flight 50) on the page router; apilimit untouched - Pentester accounts (ids 42-48) deleted in prod DB
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import { pbkdf2 } from 'crypto'
|
||||
import { promisify } from 'util'
|
||||
import type { AccessArgs, CollectionConfig } from 'payload'
|
||||
|
||||
const isAdmin = (args: AccessArgs) =>
|
||||
@@ -42,7 +44,17 @@ const Users: CollectionConfig = {
|
||||
overrideAccess: true,
|
||||
})
|
||||
const user = docs[0] as { id: number; role?: string } | undefined
|
||||
if (user?.role !== 'admin') return
|
||||
if (!user) {
|
||||
// F7: with no matching user, Payload would skip hashing and fail
|
||||
// fast, letting attackers enumerate emails via response time. Burn
|
||||
// the same PBKDF2 cost as a real verify (600000 iters / 32 bytes /
|
||||
// sha256 — same params as payload's generatePasswordSaltHash), then
|
||||
// fall through to the standard invalid-credentials error.
|
||||
const password = typeof args?.data?.password === 'string' ? args.data.password : 'x'
|
||||
await promisify(pbkdf2)(password, 'anwarpy-dummy-salt-v1', 600000, 32, 'sha256')
|
||||
return
|
||||
}
|
||||
if (user.role !== 'admin') return
|
||||
await req.payload.db.updateOne({
|
||||
collection: 'users',
|
||||
id: user.id,
|
||||
|
||||
Reference in New Issue
Block a user