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).
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.tsbeforeOperation 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.
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
- 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 main2026-10-09 17:01:00 +03:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
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
anwarpipage router (web/docker-compose.vps.yml):pagelimit: 60 req/min average, burst 120 per IPconnlimit: 50 in-flight requests per IPapilimit(API router) untouchedVerified (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/hourPOST /api/users/login: 10/15minPOST /api/users/forgot-password: 5/hourOver 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; includeSubDomainsadded to the global/:path*headers block inweb/next.config.mjs(pages, static assets,/api). Notpreload(not submitted to the preload list).Verified: header present on
/and an/apipath in prod.F7 — Login-timing user enumeration (Low)
web/src/collections/Users.tsbeforeOperationlogin hook: when no user matches the email, a dummy PBKDF2 (600000iterations / 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
kali.recon999@…,token.probe@…, 5×rl*@recon.example) — verified count 0.Verified, no change needed
verificationToken: existingbeforeChangeuntrusted branch already strips it — proven on the new build (signup withverificationToken/role:"vip"/emailVerified:truein body → response contains no token, role forcedfree,emailVerified:false).stripe-signatureverified once keys are set)./api/mediareadable: required for anonymous<img>cover rendering./admin: auth-gated, robots-disallowed.Verification summary
npm run buildexit 0 (middleware compiled:ƒ Middleware 33.5 kB)docker compose -f docker-compose.vps.yml up -d --build(exit 0, container recreated)/,/articles, post page,/api/posts/first-arabic-agentall 200; temp-user login 200 +/api/users/me200; all test rows deleted from dev and prod DBs