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:
@@ -8,6 +8,7 @@ const {
|
||||
getTokenExpiry
|
||||
} = require('../utils/auth');
|
||||
const { sendVerificationEmail, sendWelcomeEmail, sendPasswordResetEmail } = require('../utils/email');
|
||||
const { sanitizeForEmail, timingSafeEqual } = require('../utils/sanitize');
|
||||
|
||||
// Register new user (Phase 1.5 - with WSDC support and email verification)
|
||||
async function register(req, res, next) {
|
||||
@@ -25,25 +26,12 @@ async function register(req, res, next) {
|
||||
},
|
||||
});
|
||||
|
||||
// Prevent user enumeration - use generic error message
|
||||
if (existingUser) {
|
||||
if (existingUser.email === email) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Email already registered',
|
||||
});
|
||||
}
|
||||
if (existingUser.username === username) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Username already taken',
|
||||
});
|
||||
}
|
||||
if (wsdcId && existingUser.wsdcId === wsdcId) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'WSDC ID already registered',
|
||||
});
|
||||
}
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'An account with these credentials already exists. Please try logging in or use different credentials.',
|
||||
});
|
||||
}
|
||||
|
||||
// Hash password
|
||||
@@ -87,11 +75,11 @@ async function register(req, res, next) {
|
||||
},
|
||||
});
|
||||
|
||||
// Send verification email
|
||||
// Send verification email (sanitize inputs)
|
||||
try {
|
||||
await sendVerificationEmail(
|
||||
user.email,
|
||||
user.firstName || user.username,
|
||||
sanitizeForEmail(user.firstName || user.username),
|
||||
verificationToken,
|
||||
verificationCode
|
||||
);
|
||||
@@ -213,9 +201,9 @@ async function verifyEmailByToken(req, res, next) {
|
||||
},
|
||||
});
|
||||
|
||||
// Send welcome email
|
||||
// Send welcome email (sanitize inputs)
|
||||
try {
|
||||
await sendWelcomeEmail(user.email, user.firstName || user.username);
|
||||
await sendWelcomeEmail(user.email, sanitizeForEmail(user.firstName || user.username));
|
||||
} catch (emailError) {
|
||||
console.error('Failed to send welcome email:', emailError);
|
||||
}
|
||||
@@ -283,9 +271,9 @@ async function verifyEmailByCode(req, res, next) {
|
||||
},
|
||||
});
|
||||
|
||||
// Send welcome email
|
||||
// Send welcome email (sanitize inputs)
|
||||
try {
|
||||
await sendWelcomeEmail(user.email, user.firstName || user.username);
|
||||
await sendWelcomeEmail(user.email, sanitizeForEmail(user.firstName || user.username));
|
||||
} catch (emailError) {
|
||||
console.error('Failed to send welcome email:', emailError);
|
||||
}
|
||||
@@ -346,10 +334,10 @@ async function resendVerification(req, res, next) {
|
||||
},
|
||||
});
|
||||
|
||||
// Send verification email
|
||||
// Send verification email (sanitize inputs)
|
||||
await sendVerificationEmail(
|
||||
user.email,
|
||||
user.firstName || user.username,
|
||||
sanitizeForEmail(user.firstName || user.username),
|
||||
verificationToken,
|
||||
verificationCode
|
||||
);
|
||||
@@ -401,11 +389,11 @@ async function requestPasswordReset(req, res, next) {
|
||||
},
|
||||
});
|
||||
|
||||
// Send password reset email
|
||||
// Send password reset email (sanitize inputs)
|
||||
try {
|
||||
await sendPasswordResetEmail(
|
||||
user.email,
|
||||
user.firstName || user.username,
|
||||
sanitizeForEmail(user.firstName || user.username),
|
||||
resetToken
|
||||
);
|
||||
} catch (emailError) {
|
||||
@@ -437,13 +425,8 @@ async function resetPassword(req, res, next) {
|
||||
});
|
||||
}
|
||||
|
||||
// Validate password length
|
||||
if (newPassword.length < 8) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Password must be at least 8 characters long',
|
||||
});
|
||||
}
|
||||
// Password validation is now handled by validators middleware
|
||||
// No need for manual validation here
|
||||
|
||||
// Find user by reset token
|
||||
const user = await prisma.user.findUnique({
|
||||
|
||||
Reference in New Issue
Block a user