Skip to content

Security Hygiene Backlog: rate limiting, security headers, account lockout #315

@steilerDev

Description

@steilerDev

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

  1. 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
  2. Given the application is running, when any page is loaded, then security headers (HSTS, CSP, X-Frame-Options) are present in the response
  3. 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
  4. Given a budget category named "Materials" exists, when a user creates another category named "materials" (different case), then they receive a 409 conflict error
  5. 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)

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions