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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user