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:
Radosław Gierwiało
2025-11-13 16:39:27 +01:00
parent 46224fca79
commit bf8a9260bd
17 changed files with 2620 additions and 82 deletions

View 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,
};

View File

@@ -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,
};