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:
AHMED
2026-10-09 17:00:15 +03:00
parent 5ee706ebd6
commit 8e577165cf
4 changed files with 84 additions and 1 deletions
+13 -1
View File
@@ -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,