Overview
Several security hardening items have been open since Sprint 1 (identified in PR reviews #57, #150-#158). These are non-blocking but represent meaningful security improvements before production use.
Parent Epic: #10 (EPIC-10: UX Polish / Hardening)
Items to Implement
1. Rate Limiting (Medium priority — from PR #57)
- Install
@fastify/rate-limit
- Apply to:
POST /api/auth/login, POST /api/auth/setup, POST /api/users/:id/password
- Recommended limits: 10 attempts per 15 minutes per IP
- Return HTTP 429 with
RATE_LIMIT_EXCEEDED error code
2. Security Headers (Low priority — from PR #57)
- Install
@fastify/helmet
- Register before route plugins in
app.ts
- Configure CSP, HSTS, X-Frame-Options, X-Content-Type-Options
- Verify no headers break the SPA client routing
3. Account Lockout (Low priority — from PR #57)
- After N failed login attempts (e.g., 10), lock the account temporarily (or permanently until admin unlocks)
- Store
failedLoginAttempts and lockedUntil in the users table (migration required)
- Return HTTP 429 with
ACCOUNT_LOCKED error code
- Admin endpoint to unlock accounts
4. Email Format Validation (Low — from PR #151)
- Add
format: 'email' to vendor email field in Fastify JSON schema
5. 409 Error Detail Suppression (Low — from PRs #150–#187)
- Remove count details from
CATEGORY_IN_USE, VENDOR_IN_USE, BUDGET_SOURCE_IN_USE, BUDGET_LINE_IN_USE 409 response bodies
- These expose internal counts that could leak data
6. Case-Insensitive DB Unique Index (Low — from PR #150)
- Add a case-insensitive unique index on
budget_categories.name at the DB level (currently enforced only at app layer)
Acceptance Criteria
- Given a user makes 11+ login attempts in 15 minutes, when they attempt to log in, then they receive HTTP 429 with
RATE_LIMIT_EXCEEDED
- Given the application is running, when any page is loaded, then security headers (HSTS, CSP, X-Frame-Options) are present in the response
- Given a vendor has been created with an invalid email, when the user submits the form, then a 400 error with field-level validation is returned
- Given a budget category named "Materials" exists, when a user creates another category named "materials" (different case), then they receive a 409 conflict error
- Given a 409 CATEGORY_IN_USE error occurs, when the response body is inspected, then no item counts are exposed in the
details field
Notes
Tracked findings: Security Audit findings #1 (rate limiting), #2 (security headers), #3 (account lockout), #6 (email validation), #5 (409 suppression), #4 (case-insensitive index)
Overview
Several security hardening items have been open since Sprint 1 (identified in PR reviews #57, #150-#158). These are non-blocking but represent meaningful security improvements before production use.
Parent Epic: #10 (EPIC-10: UX Polish / Hardening)
Items to Implement
1. Rate Limiting (Medium priority — from PR #57)
@fastify/rate-limitPOST /api/auth/login,POST /api/auth/setup,POST /api/users/:id/passwordRATE_LIMIT_EXCEEDEDerror code2. Security Headers (Low priority — from PR #57)
@fastify/helmetapp.ts3. Account Lockout (Low priority — from PR #57)
failedLoginAttemptsandlockedUntilin theuserstable (migration required)ACCOUNT_LOCKEDerror code4. Email Format Validation (Low — from PR #151)
format: 'email'to vendor email field in Fastify JSON schema5. 409 Error Detail Suppression (Low — from PRs #150–#187)
CATEGORY_IN_USE,VENDOR_IN_USE,BUDGET_SOURCE_IN_USE,BUDGET_LINE_IN_USE409 response bodies6. Case-Insensitive DB Unique Index (Low — from PR #150)
budget_categories.nameat the DB level (currently enforced only at app layer)Acceptance Criteria
RATE_LIMIT_EXCEEDEDdetailsfieldNotes
wiki/Security-Audit.mdfindings EPIC-01: Authentication & User Management #1-EPIC-09: Dashboard & Project Health Center #9Tracked findings: Security Audit findings #1 (rate limiting), #2 (security headers), #3 (account lockout), #6 (email validation), #5 (409 suppression), #4 (case-insensitive index)