# ADR-016: API Security Hardening **Date**: 2025-12-12 **Status**: Accepted ## Context While authentication (`ADR-011`) is covered, the API lacks formal policies for protection against common web vulnerabilities and abuse. Security measures like rate limiting, secure HTTP headers, and Cross-Origin Resource Sharing (CORS) are not standardized. ## Decision We will implement a multi-layered security approach for the API: 1. **`helmet`**: Use the `helmet` library to set crucial security headers (e.g., Content-Security-Policy, X-Content-Type-Options). 2. **Rate Limiting**: Apply rate limiting using a library like `express-rate-limit` on sensitive endpoints (e.g., `/login`, `/register`) to prevent brute-force attacks. 3. **CORS**: Establish a strict CORS policy to control which domains can access the API. ## Consequences - **Positive**: Significantly improves the application's security posture against common web vulnerabilities like XSS, clickjacking, and brute-force attacks. - **Negative**: Requires careful configuration of CORS and rate limits to avoid blocking legitimate traffic. Content-Security-Policy can be complex to configure correctly. ## Implementation Status ### What's Implemented - ✅ **Helmet** - Security headers middleware with CSP, HSTS, and more - ✅ **Rate Limiting** - Comprehensive implementation with 17+ specific limiters - ✅ **Input Validation** - Zod-based request validation on all routes - ✅ **File Upload Security** - MIME type validation, size limits, filename sanitization - ✅ **Error Handling** - Production-safe error responses (no sensitive data leakage) - ✅ **Request Timeout** - 5-minute timeout protection - ✅ **Secure Cookies** - httpOnly and secure flags for authentication cookies ### Not Required - ℹ️ **CORS** - Not needed (API and frontend are same-origin) ## Implementation Details ### Helmet Security Headers Using **helmet v8.x** configured in `server.ts` as the first middleware after app initialization. **Security Headers Applied**: | Header | Configuration | Purpose | | ------ | ------------- | ------- | | Content-Security-Policy | Custom directives | Prevents XSS, code injection | | Strict-Transport-Security | 1 year, includeSubDomains, preload | Forces HTTPS connections | | X-Content-Type-Options | nosniff | Prevents MIME type sniffing | | X-Frame-Options | DENY | Prevents clickjacking | | X-XSS-Protection | 0 (disabled) | Deprecated, CSP preferred | | Referrer-Policy | strict-origin-when-cross-origin | Controls referrer information | | Cross-Origin-Resource-Policy | cross-origin | Allows external resource loading | **Content Security Policy Directives**: ```typescript contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], scriptSrc: ["'self'", "'unsafe-inline'"], // React inline scripts styleSrc: ["'self'", "'unsafe-inline'"], // Tailwind inline styles imgSrc: ["'self'", 'data:', 'blob:', 'https:'], // External images fontSrc: ["'self'", 'https:', 'data:'], connectSrc: ["'self'", 'https:', 'wss:'], // API + WebSocket frameSrc: ["'none'"], // No iframes objectSrc: ["'none'"], // No plugins upgradeInsecureRequests: [], // Production only }, } ``` **HSTS Configuration**: - Max-age: 1 year (31536000 seconds) - Includes subdomains - Preload-ready for browser HSTS lists ### Rate Limiting Using **express-rate-limit v8.2.1** with a centralized configuration in `src/config/rateLimiters.ts`. **Standard Configuration**: ```typescript const standardConfig = { standardHeaders: true, // Sends RateLimit-* headers legacyHeaders: false, skip: shouldSkipRateLimit, // Disabled in test environment }; ``` **Rate Limiters by Category**: | Category | Limiter | Window | Max Requests | | -------- | ------- | ------ | ------------ | | **Authentication** | loginLimiter | 15 min | 5 | | | registerLimiter | 1 hour | 5 | | | forgotPasswordLimiter | 15 min | 5 | | | resetPasswordLimiter | 15 min | 10 | | | refreshTokenLimiter | 15 min | 20 | | | logoutLimiter | 15 min | 10 | | **Public/User Read** | publicReadLimiter | 15 min | 100 | | | userReadLimiter | 15 min | 100 | | | userUpdateLimiter | 15 min | 100 | | **Sensitive Operations** | userSensitiveUpdateLimiter | 1 hour | 5 | | | adminTriggerLimiter | 15 min | 30 | | **AI/Costly** | aiGenerationLimiter | 15 min | 20 | | | geocodeLimiter | 1 hour | 100 | | | priceHistoryLimiter | 15 min | 50 | | **Uploads** | adminUploadLimiter | 15 min | 20 | | | aiUploadLimiter | 15 min | 10 | | | batchLimiter | 15 min | 50 | | **Tracking** | trackingLimiter | 15 min | 200 | | | reactionToggleLimiter | 15 min | 150 | **Test Environment Handling**: Rate limiting is automatically disabled in test environment via `shouldSkipRateLimit` utility (`src/utils/rateLimit.ts`). Tests can opt-in to rate limiting by setting the `x-test-rate-limit-enable: true` header. ### Input Validation **Zod Schema Validation** (`src/middleware/validation.middleware.ts`): - Type-safe parsing and coercion for params, query, and body - Applied to all API routes via `validateRequest()` middleware - Returns structured validation errors with field-level details **Filename Sanitization** (`src/utils/stringUtils.ts`): ```typescript // Removes dangerous characters from uploaded filenames sanitizeFilename(filename: string): string ``` ### File Upload Security **Multer Configuration** (`src/middleware/multer.middleware.ts`): - MIME type validation via `imageFileFilter` (only image/* allowed) - File size limits (2MB for logos, configurable per upload type) - Unique filenames using timestamps + random suffixes - User-scoped storage paths ### Error Handling **Production-Safe Responses** (`src/middleware/errorHandler.ts`): - Production mode: Returns generic error message with tracking ID - Development mode: Returns detailed error information - Sensitive error details are logged but never exposed to clients ### Request Security **Timeout Protection** (`server.ts`): - 5-minute request timeout via `connect-timeout` middleware - Prevents resource exhaustion from long-running requests **Secure Cookies**: ```typescript // Cookie configuration for auth tokens { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'strict', maxAge: 7 * 24 * 60 * 60 * 1000 // 7 days for refresh token } ``` ### Request Logging Per-request structured logging (ADR-004): - Request ID tracking - User ID and IP address logging - Failed request details (4xx+) logged with headers and body - Unhandled errors assigned unique error IDs ## Key Files - `server.ts` - Helmet middleware configuration (security headers) - `src/config/rateLimiters.ts` - Rate limiter definitions (17+ limiters) - `src/utils/rateLimit.ts` - Rate limit skip logic for testing - `src/middleware/validation.middleware.ts` - Zod-based request validation - `src/middleware/errorHandler.ts` - Production-safe error handling - `src/middleware/multer.middleware.ts` - Secure file upload configuration - `src/utils/stringUtils.ts` - Filename sanitization ## Future Enhancements 1. **Configure CORS** (if needed for cross-origin access): ```bash npm install cors @types/cors ``` Add to `server.ts`: ```typescript import cors from 'cors'; app.use(cors({ origin: process.env.ALLOWED_ORIGINS?.split(',') || 'http://localhost:3000', credentials: true, })); ``` 2. **Redis-backed rate limiting**: For distributed deployments, use `rate-limit-redis` store 3. **CSP Nonce**: Generate per-request nonces for stricter script-src policy 4. **Report-Only CSP**: Add `Content-Security-Policy-Report-Only` header for testing policy changes