security: implement CRITICAL and MEDIUM security fixes with environment profiles
This commit addresses all CRITICAL and MEDIUM security vulnerabilities
identified in the security audit with environment-aware configuration.
## Docker Compose Profiles
- Added docker-compose.dev.yml for development (relaxed security)
- Added docker-compose.prod.yml for production (strict security)
- Environment-specific configurations for rate limiting, CSRF, logging
## CRITICAL Fixes (P0)
1. Fixed insecure random number generation
- Replaced Math.random() with crypto.randomBytes() for verification codes
- Now cryptographically secure
2. Implemented rate limiting
- express-rate-limit for all endpoints
- Strict limits on auth endpoints (5 attempts in dev=off, prod=5)
- Email endpoint limits (20 in dev, 3 in prod)
- API-wide rate limiting
3. Added request body size limits
- Development: 50MB (for testing)
- Production: 10KB (security)
4. Fixed user enumeration vulnerability
- Generic error message for registration
- No disclosure of which field exists
5. Added security headers
- helmet.js with CSP, HSTS, XSS protection
- No-sniff, hide powered-by headers
## MEDIUM Fixes (P1)
6. Strengthened password policy
- Environment-aware validation (8+ chars)
- Production: requires uppercase, lowercase, number
- Development: relaxed for testing
7. Enhanced input validation
- Validation for all auth endpoints
- WSDC ID validation (numeric, max 10 digits)
- Name validation (safe characters only)
- Email normalization
8. Added input sanitization
- DOMPurify for XSS prevention
- Sanitize all user inputs in emails
- Timing-safe string comparison for tokens
9. Improved error handling
- Generic errors in production
- Detailed errors only in development
- Proper error logging
10. Enhanced CORS configuration
- Whitelist-based origin validation
- Environment-specific allowed origins
- Credentials support
## New Files
- backend/src/config/security.js - Environment-aware security config
- backend/src/middleware/rateLimiter.js - Rate limiting middleware
- backend/src/utils/sanitize.js - Input sanitization utilities
- backend/.env.example - Development environment template
- backend/.env.production.example - Production environment template
- docker-compose.dev.yml - Development overrides
- docker-compose.prod.yml - Production configuration
- docs/DEPLOYMENT.md - Complete deployment guide
- docs/SECURITY_AUDIT.md - Full security audit report
- .gitignore - Updated to exclude .env files
## Dependencies Added
- helmet (^8.1.0) - Security headers
- express-rate-limit (^8.2.1) - Rate limiting
- dompurify (^3.3.0) - XSS prevention
- jsdom (^27.2.0) - DOM manipulation for sanitization
## Testing
- ✅ Password validation works (weak passwords rejected)
- ✅ User enumeration fixed (generic error messages)
- ✅ WSDC lookup functional
- ✅ Registration flow working
- ✅ Rate limiting active (environment-aware)
- ✅ Security headers present
## Usage
Development:
docker compose -f docker-compose.yml -f docker-compose.dev.yml up
Production:
docker compose -f docker-compose.yml -f docker-compose.prod.yml up
See docs/DEPLOYMENT.md for detailed instructions.
This commit is contained in:
58
backend/src/middleware/rateLimiter.js
Normal file
58
backend/src/middleware/rateLimiter.js
Normal file
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* Rate Limiting Middleware
|
||||
* Protects against brute force and DoS attacks
|
||||
*/
|
||||
|
||||
const rateLimit = require('express-rate-limit');
|
||||
const securityConfig = require('../config/security');
|
||||
|
||||
// Create rate limiters based on configuration
|
||||
|
||||
// General API rate limiter
|
||||
const apiLimiter = rateLimit({
|
||||
windowMs: securityConfig.rateLimit.api.windowMs,
|
||||
max: securityConfig.rateLimit.api.max,
|
||||
message: {
|
||||
success: false,
|
||||
error: 'Too Many Requests',
|
||||
message: 'Too many requests from this IP, please try again later.',
|
||||
},
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
skip: () => !securityConfig.rateLimit.enabled,
|
||||
});
|
||||
|
||||
// Strict limiter for authentication endpoints (login, register)
|
||||
const authLimiter = rateLimit({
|
||||
windowMs: securityConfig.rateLimit.auth.windowMs,
|
||||
max: securityConfig.rateLimit.auth.max,
|
||||
skipSuccessfulRequests: securityConfig.rateLimit.auth.skipSuccessfulRequests,
|
||||
message: {
|
||||
success: false,
|
||||
error: 'Too Many Login Attempts',
|
||||
message: 'Too many authentication attempts from this IP, please try again in 15 minutes.',
|
||||
},
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
skip: () => !securityConfig.rateLimit.enabled,
|
||||
});
|
||||
|
||||
// Email limiter (verification, password reset)
|
||||
const emailLimiter = rateLimit({
|
||||
windowMs: securityConfig.rateLimit.email.windowMs,
|
||||
max: securityConfig.rateLimit.email.max,
|
||||
message: {
|
||||
success: false,
|
||||
error: 'Too Many Email Requests',
|
||||
message: 'Too many email requests from this IP, please try again later.',
|
||||
},
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
skip: () => !securityConfig.rateLimit.enabled,
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
apiLimiter,
|
||||
authLimiter,
|
||||
emailLimiter,
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
const { body, validationResult } = require('express-validator');
|
||||
const securityConfig = require('../config/security');
|
||||
|
||||
// Validation error handler
|
||||
function handleValidationErrors(req, res, next) {
|
||||
@@ -13,6 +14,33 @@ function handleValidationErrors(req, res, next) {
|
||||
next();
|
||||
}
|
||||
|
||||
// Password validation builder (environment-aware)
|
||||
function buildPasswordValidation(field = 'password') {
|
||||
const { minLength, requireUppercase, requireLowercase, requireNumber, requireSpecial } = securityConfig.password;
|
||||
|
||||
let validator = body(field)
|
||||
.isLength({ min: minLength, max: 128 })
|
||||
.withMessage(`Password must be between ${minLength} and 128 characters`);
|
||||
|
||||
if (requireUppercase || requireLowercase || requireNumber) {
|
||||
let pattern = '^';
|
||||
if (requireUppercase) pattern += '(?=.*[A-Z])';
|
||||
if (requireLowercase) pattern += '(?=.*[a-z])';
|
||||
if (requireNumber) pattern += '(?=.*\\d)';
|
||||
if (requireSpecial) pattern += '(?=.*[@$!%*?&#])';
|
||||
|
||||
validator = validator.matches(new RegExp(pattern))
|
||||
.withMessage('Password must contain ' + [
|
||||
requireUppercase && 'uppercase letter',
|
||||
requireLowercase && 'lowercase letter',
|
||||
requireNumber && 'number',
|
||||
requireSpecial && 'special character',
|
||||
].filter(Boolean).join(', '));
|
||||
}
|
||||
|
||||
return validator;
|
||||
}
|
||||
|
||||
// Register validation rules
|
||||
const registerValidation = [
|
||||
body('username')
|
||||
@@ -26,9 +54,26 @@ const registerValidation = [
|
||||
.isEmail()
|
||||
.withMessage('Must be a valid email address')
|
||||
.normalizeEmail(),
|
||||
body('password')
|
||||
.isLength({ min: 6 })
|
||||
.withMessage('Password must be at least 6 characters long'),
|
||||
buildPasswordValidation('password'),
|
||||
body('firstName')
|
||||
.optional()
|
||||
.trim()
|
||||
.isLength({ max: 100 })
|
||||
.withMessage('First name must be less than 100 characters')
|
||||
.matches(/^[a-zA-ZÀ-ÿ\s'-]+$/)
|
||||
.withMessage('First name contains invalid characters'),
|
||||
body('lastName')
|
||||
.optional()
|
||||
.trim()
|
||||
.isLength({ max: 100 })
|
||||
.withMessage('Last name must be less than 100 characters')
|
||||
.matches(/^[a-zA-ZÀ-ÿ\s'-]+$/)
|
||||
.withMessage('Last name contains invalid characters'),
|
||||
body('wsdcId')
|
||||
.optional()
|
||||
.trim()
|
||||
.matches(/^\d{1,10}$/)
|
||||
.withMessage('WSDC ID must be numeric (max 10 digits)'),
|
||||
handleValidationErrors,
|
||||
];
|
||||
|
||||
@@ -45,8 +90,33 @@ const loginValidation = [
|
||||
handleValidationErrors,
|
||||
];
|
||||
|
||||
// Verify code validation
|
||||
const verifyCodeValidation = [
|
||||
body('email')
|
||||
.trim()
|
||||
.isEmail()
|
||||
.withMessage('Must be a valid email address')
|
||||
.normalizeEmail(),
|
||||
body('code')
|
||||
.trim()
|
||||
.matches(/^\d{6}$/)
|
||||
.withMessage('Code must be 6 digits'),
|
||||
handleValidationErrors,
|
||||
];
|
||||
|
||||
// Password reset validation
|
||||
const passwordResetValidation = [
|
||||
body('token')
|
||||
.notEmpty()
|
||||
.withMessage('Reset token is required'),
|
||||
buildPasswordValidation('newPassword'),
|
||||
handleValidationErrors,
|
||||
];
|
||||
|
||||
module.exports = {
|
||||
registerValidation,
|
||||
loginValidation,
|
||||
verifyCodeValidation,
|
||||
passwordResetValidation,
|
||||
handleValidationErrors,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user