fix(auth): rate-limit + body-cap the public register endpoint (TODO 8/9 §B) - #106
fix(auth): rate-limit + body-cap the public register endpoint (TODO 8/9 §B)#106lopugit wants to merge 3 commits into
Conversation
POST /api/v1/auth/register had no throttle and no body cap: each request costs a bcrypt hash + a verification email to any caller-named address, so it was brute-forceable and a mail-bomb/DoS-amplification vector at full speed (the last unhardened auth endpoint — login, resend-verification and password-reset already enforce the shared limiter). - New auth.register rule in rateLimit/config.ts: 10 per 15 min per IP, same bucket family as auth.passwordReset/auth.resendVerification. - Enforce it before the body read (a burst of empty-body 400s still trips the cap), returning 429 + Retry-After via the existing helpers. - Read the body through readJsonBody(request, 16*1024) so an oversize payload is rejected with 413 before any bcrypt/DB work. Seeding is unaffected: setup.ts calls registerUser() directly and never crosses the HTTP limiter. meta is already stripped of privileged keys at the createUserAccount chokepoint and register never forwards caller meta. Verified on a live dev stack: 10 empty-body signups 400, the 11th 429, oversize body 413, a fresh-IP valid signup returns ok:true; per-IP buckets isolate (distinct X-Forwarded-For not throttled). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Conflicted files (as recorded by the merge step):
|
|
Conflicted files (as recorded by the merge step):
|
|
Conflicted files (as recorded by the merge step):
|
|
Conflicted files (as recorded by the merge step):
|
|
Conflicted files (as recorded by the merge step):
|
|
Conflicted files (as recorded by the merge step):
|
…licts) Conflicted paths: TESTING.md Resolved by the resolve-pr-conflicts workflow: https://github.com/lopugit/thingtime/actions/runs/30540294630 Co-Authored-By: Claude <noreply@anthropic.com>
|
🤝 Merged Conflicted files:
Please review the merge commit before relying on it. |
…98/#106) Upgrade the persist codec from strict-ISO string revival to the tagged scheme the duplicate TODO-9 PRs used, closing the remaining false-positive: - Real Dates persist as {ttype:'date', iso}. The replacer reads the original off the holder (this[key]) because Date.toJSON converts Dates to strings before the replacer sees the value. - A USER string that merely looks like a full ISO timestamp is escaped as {ttype:'iso-string', s} so the legacy fallback can never capture it — previously such a string became a Date on the next load. - The legacy bare-ISO fallback remains only to migrate pre-tagging persists. - Both replacer and reviver skip their own wrappers' inner keys (without the guard the escape rule re-wraps its own output unboundedly, and the legacy fallback hands the wrapper branches a Date instead of the original string — caught by the new tests). Tests: 9/9 in test:persist, including Date + identical-looking string coexisting, two-cycle string stability, and hostile-payload drops. TESTING.md gains the register rate-limit checklist (from #106) and a persisted-codec checklist. Live-verified: app hydrates under the CSP, persists and reloads cleanly, zero console errors. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Advances TODO #8 /
claude-todo/09-security-hardening.md§B:POST /api/v1/auth/registerwas the last unhardened public auth endpoint.login,resend-verification, andpassword-resetalready enforce the shared Mongo-backed rate limiter, but register had no throttle and no body cap — each request costs a bcrypt hash plus a verification email to any caller-named address, so it was brute-forceable and a mail-bomb / DoS-amplification vector at full speed.What changed
auth.registerrule inrateLimit/config.ts: 10 per 15 min per IP — same bucket family and window asauth.passwordReset/auth.resendVerification, admin-overridable like every other rule. Single source of truth; no second limiter invented._register.tsx, so a burst of empty-body signups (each a 400) still trips the cap. Returns 429 +Retry-Aftervia the existingrateLimitedResponseInithelper.readJsonBody(request, 16*1024)(the same idiom the things/auth routes already use) → 413 on oversize before any bcrypt/DB work.Not touched because already safe:
metamass-assignment — register never forwards callermeta, and privileged keys are stripped again at thecreateUserAccountchokepoint (sanitizeCreateMeta). Service-account (A3) throttling is a sibling branch (claude/service-account-provisioning-hardening, PR #100); raw-results (A1) and populate (A2) already gainedrequireAdmin+failClosedlimiters onmain. This PR is scoped to the register gap and does not overlap them.Verification (live dev stack, worktree, ports 9977/9979)
Exercised the running Nitro action (the exact path the browser hits), using distinct
X-Forwarded-Forvalues to get isolated per-IP buckets:ok:truewith a real user id + auth cookie.registerAllinsetup.tscallsregisterUser()directly and never crosses the HTTP limiter.Cleared the test rate-limit buckets from the local dev DB afterward. Added a regression checklist section to
TESTING.mdand ticked the two now-satisfied boxes in the security-hardening spec.🤖 Generated with Claude Code