Some checks failed
Deploy to Test Environment / deploy-to-test (push) Has been cancelled
217 lines
7.6 KiB
Markdown
217 lines
7.6 KiB
Markdown
# 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
|