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

@@ -34,9 +34,13 @@ function generateVerificationToken() {
return crypto.randomBytes(32).toString('hex');
}
// Generate 6-digit verification code
// Generate 6-digit verification code (cryptographically secure)
function generateVerificationCode() {
return Math.floor(100000 + Math.random() * 900000).toString();
// Use crypto.randomBytes for cryptographically secure random numbers
const bytes = crypto.randomBytes(4);
const num = bytes.readUInt32BE(0);
// Ensure 6 digits (100000 to 999999)
return String(num % 900000 + 100000);
}
// Calculate token expiry time

View File

@@ -0,0 +1,80 @@
/**
* Input Sanitization Utilities
* Prevents XSS and injection attacks
*/
const createDOMPurify = require('dompurify');
const { JSDOM } = require('jsdom');
const window = new JSDOM('').window;
const DOMPurify = createDOMPurify(window);
/**
* Sanitize HTML input to prevent XSS
* @param {string} dirty - Untrusted HTML string
* @returns {string} - Sanitized string
*/
function sanitizeHtml(dirty) {
if (typeof dirty !== 'string') return '';
return DOMPurify.sanitize(dirty, {
ALLOWED_TAGS: [], // Strip all HTML tags
ALLOWED_ATTR: [],
});
}
/**
* Sanitize text for use in emails
* @param {string} text - User input text
* @returns {string} - Sanitized text
*/
function sanitizeForEmail(text) {
if (typeof text !== 'string') return '';
// Remove HTML tags and encode special characters
return DOMPurify.sanitize(text, {
ALLOWED_TAGS: [],
ALLOWED_ATTR: [],
}).trim();
}
/**
* Sanitize username (alphanumeric + underscore only)
* @param {string} username - Username input
* @returns {string} - Sanitized username
*/
function sanitizeUsername(username) {
if (typeof username !== 'string') return '';
return username.replace(/[^a-zA-Z0-9_]/g, '').trim();
}
/**
* Timing-safe string comparison
* Prevents timing attacks on token comparison
* @param {string} a - First string
* @param {string} b - Second string
* @returns {boolean} - True if strings match
*/
function timingSafeEqual(a, b) {
const crypto = require('crypto');
if (typeof a !== 'string' || typeof b !== 'string') return false;
if (a.length !== b.length) return false;
try {
return crypto.timingSafeEqual(
Buffer.from(a, 'utf8'),
Buffer.from(b, 'utf8')
);
} catch (err) {
return false;
}
}
module.exports = {
sanitizeHtml,
sanitizeForEmail,
sanitizeUsername,
timingSafeEqual,
};