security: implement multiple security hardening fixes

This commit addresses several critical and high-severity security
vulnerabilities identified in a comprehensive security audit:

Critical Fixes:
- Enforce JWT_SECRET in production (throws error if not set)
- Add JWT token expiration (24 hours)
- Implement path traversal protection for static file serving
- Add rate limiting to authentication endpoints (5/10 req per minute)
- Fix CORS to never allow all origins (even in development)

High/Medium Fixes:
- Add comprehensive security headers (CSP, HSTS, X-Frame-Options, etc.)
- Implement stricter email validation (RFC 5321 compliant)
- Add input sanitization for search parameters (length limit, wildcard removal)
- Improve job/QSO ID validation (range checks, safe integer validation)

Files modified:
- src/backend/config.js: JWT secret enforcement
- src/backend/index.js: JWT expiration, security headers, rate limiting,
  email validation, path traversal protection, CORS hardening, ID validation
- src/backend/services/lotw.service.js: search input sanitization

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-01-20 17:40:31 +01:00
parent 2aebfb0771
commit db0145782a
3 changed files with 283 additions and 36 deletions

View File

@@ -15,6 +15,35 @@ const MAX_RETRIES = 30;
const RETRY_DELAY = 10000;
const REQUEST_TIMEOUT = 60000;
/**
* SECURITY: Sanitize search input to prevent injection and DoS
* Limits length and removes potentially harmful characters
*/
function sanitizeSearchInput(searchTerm) {
if (!searchTerm || typeof searchTerm !== 'string') {
return '';
}
// Trim whitespace
let sanitized = searchTerm.trim();
// Limit length (DoS prevention)
const MAX_SEARCH_LENGTH = 100;
if (sanitized.length > MAX_SEARCH_LENGTH) {
sanitized = sanitized.substring(0, MAX_SEARCH_LENGTH);
}
// Remove potentially dangerous SQL pattern wildcards from user input
// We'll add our own wildcards for the LIKE query
// Note: Drizzle ORM escapes parameters, but this adds defense-in-depth
sanitized = sanitized.replace(/[%_\\]/g, '');
// Remove null bytes and other control characters
sanitized = sanitized.replace(/[\x00-\x1F\x7F]/g, '');
return sanitized;
}
/**
* Check if LoTW response indicates the report is still being prepared
*/
@@ -419,12 +448,16 @@ export async function getUserQSOs(userId, filters = {}, options = {}) {
// Search filter: callsign, entity, or grid
if (filters.search) {
const searchTerm = `%${filters.search}%`;
conditions.push(or(
like(qsos.callsign, searchTerm),
like(qsos.entity, searchTerm),
like(qsos.grid, searchTerm)
));
// SECURITY: Sanitize search input to prevent injection
const sanitized = sanitizeSearchInput(filters.search);
if (sanitized) {
const searchTerm = `%${sanitized}%`;
conditions.push(or(
like(qsos.callsign, searchTerm),
like(qsos.entity, searchTerm),
like(qsos.grid, searchTerm)
));
}
}
// Use SQL COUNT for efficient pagination (avoids loading all QSOs into memory)