7.6 KiB
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:
helmet: Use thehelmetlibrary to set crucial security headers (e.g., Content-Security-Policy, X-Content-Type-Options).- Rate Limiting: Apply rate limiting using a library like
express-rate-limiton sensitive endpoints (e.g.,/login,/register) to prevent brute-force attacks. - 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:
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:
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):
// 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-timeoutmiddleware - Prevents resource exhaustion from long-running requests
Secure Cookies:
// 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 testingsrc/middleware/validation.middleware.ts- Zod-based request validationsrc/middleware/errorHandler.ts- Production-safe error handlingsrc/middleware/multer.middleware.ts- Secure file upload configurationsrc/utils/stringUtils.ts- Filename sanitization
Future Enhancements
-
Configure CORS (if needed for cross-origin access):
npm install cors @types/corsAdd to
server.ts:import cors from 'cors'; app.use(cors({ origin: process.env.ALLOWED_ORIGINS?.split(',') || 'http://localhost:3000', credentials: true, })); -
Redis-backed rate limiting: For distributed deployments, use
rate-limit-redisstore -
CSP Nonce: Generate per-request nonces for stricter script-src policy
-
Report-Only CSP: Add
Content-Security-Policy-Report-Onlyheader for testing policy changes