Security fixes from pentest 2026-10-09: auth rate limits, timing equalization, HSTS, edge DoS caps #1

Merged
ahmed merged 1 commits from security/pentest-fixes-2026-10-09 into main 2026-10-09 17:01:00 +03:00
Owner

Response to the anwarpy.ai pentest report (2026-10-09). No site flow/UX changes.

Fixed

F1 — Single-box DoS froze pages (High)

Traefik edge caps on the anwarpi page router (web/docker-compose.vps.yml):

  • pagelimit: 60 req/min average, burst 120 per IP
  • connlimit: 50 in-flight requests per IP
  • apilimit (API router) untouched

Verified (owner-authorized modest test): 200-request flood at concurrency 20 → 127×200, 73×429, zero timeouts, zero 5xx; immediate recovery curl 200 in 0.69s.

F2 — Unlimited anonymous signup/login (Medium)

New web/src/middleware.ts: per-IP in-memory limits on the auth endpoints —

  • POST /api/users (signup): 5/hour
  • POST /api/users/login: 10/15min
  • POST /api/users/forgot-password: 5/hour

Over limit → 429 JSON محاولات كثيرة جدًا. حاول بعد قليل. + Retry-After. The auth pages already show a generic error on non-ok responses, so no UI change. Single-container deployment → in-memory is correct (comment in file notes Redis requirement if scaling horizontally).

Verified: local 5×201 → 6th 429; login 10×401 → 11th 429; prod signup 5×201 → 6th 429.

F4 — No HSTS (Low)

Strict-Transport-Security: max-age=63072000; includeSubDomains added to the global /:path* headers block in web/next.config.mjs (pages, static assets, /api). Not preload (not submitted to the preload list).

Verified: header present on / and an /api path in prod.

F7 — Login-timing user enumeration (Low)

web/src/collections/Users.ts beforeOperation login hook: when no user matches the email, a dummy PBKDF2 (600000 iterations / 32 bytes / sha256 — identical cost to Payload's real verify) runs before the standard invalid-credentials error, so ghost emails are no longer distinguishable by response time. Admin-unlock branch unchanged.

Verified: wrong-password login real email ≈107ms vs ghost ≈86ms steady-state (Δ ≈ 21ms, was ~300ms); raw pbkdf2@600k measured 47ms confirming the dummy hash executes.

Housekeeping

  • Pentester throwaway accounts deleted from the prod DB (ids 42–48: kali.recon999@…, token.probe@…, 5× rl*@recon.example) — verified count 0.

Verified, no change needed

  • F3 client-writable verificationToken: existing beforeChange untrusted branch already strips it — proven on the new build (signup with verificationToken/role:"vip"/emailVerified:true in body → response contains no token, role forced free, emailVerified:false).
  • F5 CUPS port 631: owner decision 2026-10-09 — leave open.
  • F6 Stripe webhook: correct pending behavior (503 until configured; stripe-signature verified once keys are set).
  • F8 /api/media readable: required for anonymous <img> cover rendering.
  • F9 /admin: auth-gated, robots-disallowed.

Verification summary

  • npm run build exit 0 (middleware compiled: ƒ Middleware 33.5 kB)
  • Deployed via rsync + docker compose -f docker-compose.vps.yml up -d --build (exit 0, container recreated)
  • Flow regression in prod: /, /articles, post page, /api/posts/first-arabic-agent all 200; temp-user login 200 + /api/users/me 200; all test rows deleted from dev and prod DBs
Response to the anwarpy.ai pentest report (2026-10-09). No site flow/UX changes. ## Fixed ### F1 — Single-box DoS froze pages (High) Traefik edge caps on the `anwarpi` page router (`web/docker-compose.vps.yml`): - `pagelimit`: 60 req/min average, burst 120 per IP - `connlimit`: 50 in-flight requests per IP - `apilimit` (API router) untouched **Verified (owner-authorized modest test):** 200-request flood at concurrency 20 → 127×200, 73×429, **zero timeouts, zero 5xx**; immediate recovery curl 200 in 0.69s. ### F2 — Unlimited anonymous signup/login (Medium) New `web/src/middleware.ts`: per-IP in-memory limits on the auth endpoints — - `POST /api/users` (signup): 5/hour - `POST /api/users/login`: 10/15min - `POST /api/users/forgot-password`: 5/hour Over limit → 429 JSON `محاولات كثيرة جدًا. حاول بعد قليل.` + `Retry-After`. The auth pages already show a generic error on non-ok responses, so **no UI change**. Single-container deployment → in-memory is correct (comment in file notes Redis requirement if scaling horizontally). **Verified:** local 5×201 → 6th 429; login 10×401 → 11th 429; prod signup 5×201 → 6th 429. ### F4 — No HSTS (Low) `Strict-Transport-Security: max-age=63072000; includeSubDomains` added to the global `/:path*` headers block in `web/next.config.mjs` (pages, static assets, `/api`). Not `preload` (not submitted to the preload list). **Verified:** header present on `/` and an `/api` path in prod. ### F7 — Login-timing user enumeration (Low) `web/src/collections/Users.ts` `beforeOperation` login hook: when no user matches the email, a dummy PBKDF2 (`600000` iterations / 32 bytes / sha256 — identical cost to Payload's real verify) runs before the standard invalid-credentials error, so ghost emails are no longer distinguishable by response time. Admin-unlock branch unchanged. **Verified:** wrong-password login real email ≈107ms vs ghost ≈86ms steady-state (**Δ ≈ 21ms**, was ~300ms); raw pbkdf2@600k measured 47ms confirming the dummy hash executes. ## Housekeeping - Pentester throwaway accounts deleted from the prod DB (ids 42–48: `kali.recon999@…`, `token.probe@…`, 5× `rl*@recon.example`) — verified count 0. ## Verified, no change needed - **F3** client-writable `verificationToken`: existing `beforeChange` untrusted branch already strips it — proven on the new build (signup with `verificationToken`/`role:"vip"`/`emailVerified:true` in body → response contains no token, role forced `free`, `emailVerified:false`). - **F5** CUPS port 631: owner decision 2026-10-09 — leave open. - **F6** Stripe webhook: correct pending behavior (503 until configured; `stripe-signature` verified once keys are set). - **F8** `/api/media` readable: required for anonymous `<img>` cover rendering. - **F9** `/admin`: auth-gated, robots-disallowed. ## Verification summary - `npm run build` exit 0 (middleware compiled: `ƒ Middleware 33.5 kB`) - Deployed via rsync + `docker compose -f docker-compose.vps.yml up -d --build` (exit 0, container recreated) - Flow regression in prod: `/`, `/articles`, post page, `/api/posts/first-arabic-agent` all 200; temp-user login 200 + `/api/users/me` 200; all test rows deleted from dev and prod DBs
ahmed added 1 commit 2026-10-09 17:00:57 +03:00
- 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
ahmed merged commit e69e419721 into main 2026-10-09 17:01:00 +03:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: ahmed/anwar-pi-website#1