const express = require('express'); const router = express.Router(); const { authenticate } = require('../middleware/auth'); const { requirePermission } = require('../middleware/rbac'); const { modifyLimiter, readLimiter } = require('../middleware/rateLimiter'); const { db } = require('../database/db'); const logger = require('../utils/logger'); const fs = require('fs').promises; const path = require('path'); /** * Security Headers Configuration Management * Allows admins to view and configure HTTP security headers */ // Create security_headers_config table db.run(` CREATE TABLE IF NOT EXISTS security_headers_config ( id INTEGER PRIMARY KEY AUTOINCREMENT, config_name TEXT NOT NULL UNIQUE, csp_default_src TEXT, csp_script_src TEXT, csp_style_src TEXT, csp_img_src TEXT, csp_media_src TEXT, csp_connect_src TEXT, csp_font_src TEXT, csp_frame_src TEXT, csp_object_src TEXT, csp_base_uri TEXT, csp_form_action TEXT, csp_frame_ancestors TEXT, hsts_enabled INTEGER DEFAULT 1, hsts_max_age INTEGER DEFAULT 31536000, hsts_include_subdomains INTEGER DEFAULT 1, hsts_preload INTEGER DEFAULT 1, referrer_policy TEXT DEFAULT 'strict-origin-when-cross-origin', x_content_type_options INTEGER DEFAULT 1, x_frame_options TEXT DEFAULT 'SAMEORIGIN', x_xss_protection INTEGER DEFAULT 1, is_active INTEGER DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_by INTEGER, FOREIGN KEY (created_by) REFERENCES users(id) ) `); // Create security_headers_history table for audit trail db.run(` CREATE TABLE IF NOT EXISTS security_headers_history ( id INTEGER PRIMARY KEY AUTOINCREMENT, config_id INTEGER, action TEXT NOT NULL, previous_config TEXT, new_config TEXT, changed_by INTEGER, timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (config_id) REFERENCES security_headers_config(id), FOREIGN KEY (changed_by) REFERENCES users(id) ) `); // Security presets const SECURITY_PRESETS = { strict: { name: 'Strict Security', description: 'Maximum security - blocks most external resources', config: { csp_default_src: "['self']", csp_script_src: "['self']", csp_style_src: "['self']", csp_img_src: "['self', 'data:', 'https:']", csp_media_src: "['self']", csp_connect_src: "['self']", csp_font_src: "['self', 'data:']", csp_frame_src: "['self']", csp_object_src: "['none']", csp_base_uri: "['self']", csp_form_action: "['self']", csp_frame_ancestors: "['self']", hsts_enabled: 1, hsts_max_age: 31536000, hsts_include_subdomains: 1, hsts_preload: 1, referrer_policy: 'no-referrer', x_content_type_options: 1, x_frame_options: 'DENY', x_xss_protection: 1 } }, balanced: { name: 'Balanced', description: 'Good security with common CDN support', config: { csp_default_src: "['self']", csp_script_src: "['self', 'https://www.gstatic.com', 'https://cdn.jsdelivr.net']", csp_style_src: "['self', 'https://fonts.googleapis.com', \"'unsafe-inline'\"]", csp_img_src: "['self', 'data:', 'blob:', 'https:', 'http:']", csp_media_src: "['self', 'blob:', 'data:', 'https:', 'http:']", csp_connect_src: "['self', 'https:', 'http:', 'ws:', 'wss:']", csp_font_src: "['self', 'data:', 'https://fonts.gstatic.com']", csp_frame_src: "['self', 'https://www.youtube.com', 'https://player.vimeo.com']", csp_object_src: "['none']", csp_base_uri: "['self']", csp_form_action: "['self']", csp_frame_ancestors: "['self']", hsts_enabled: 1, hsts_max_age: 31536000, hsts_include_subdomains: 1, hsts_preload: 0, referrer_policy: 'strict-origin-when-cross-origin', x_content_type_options: 1, x_frame_options: 'SAMEORIGIN', x_xss_protection: 1 } }, permissive: { name: 'Permissive (IPTV Streaming)', description: 'Allows external streams and APIs - current default', config: { csp_default_src: "['self']", csp_script_src: "['self', \"'unsafe-inline'\", \"'unsafe-eval'\", 'https://www.gstatic.com', 'https://cdn.jsdelivr.net', 'blob:']", csp_style_src: "['self', \"'unsafe-inline'\", 'https://fonts.googleapis.com']", csp_img_src: "['self', 'data:', 'blob:', 'https:', 'http:']", csp_media_src: "['self', 'blob:', 'data:', 'mediastream:', 'https:', 'http:', '*']", csp_connect_src: "['self', 'https:', 'http:', 'ws:', 'wss:', 'blob:', '*']", csp_font_src: "['self', 'data:', 'https://fonts.gstatic.com']", csp_frame_src: "['self', 'https://www.youtube.com', 'https://player.vimeo.com']", csp_object_src: "['none']", csp_base_uri: "['self']", csp_form_action: "['self']", csp_frame_ancestors: "['self']", hsts_enabled: 1, hsts_max_age: 31536000, hsts_include_subdomains: 1, hsts_preload: 1, referrer_policy: 'strict-origin-when-cross-origin', x_content_type_options: 1, x_frame_options: 'SAMEORIGIN', x_xss_protection: 1 } } }; // Get current active security headers configuration router.get('/current', authenticate, requirePermission('security.view_audit'), readLimiter, async (req, res) => { try { // Read current configuration from server.js const serverPath = path.join(__dirname, '../server.js'); const serverContent = await fs.readFile(serverPath, 'utf8'); // Parse current CSP configuration const currentConfig = { environment: process.env.NODE_ENV || 'development', csp: { mode: process.env.NODE_ENV === 'production' ? 'enforcing' : 'report-only', directives: extractCSPFromCode(serverContent) }, hsts: { enabled: process.env.NODE_ENV === 'production', maxAge: 31536000, includeSubDomains: true, preload: true }, referrerPolicy: 'strict-origin-when-cross-origin', xContentTypeOptions: true, xFrameOptions: 'SAMEORIGIN', xssProtection: true }; // Get saved configurations db.all( 'SELECT * FROM security_headers_config ORDER BY is_active DESC, updated_at DESC', [], (err, configs) => { if (err) { logger.error('Error fetching security headers configs:', err); return res.status(500).json({ error: 'Failed to fetch configurations' }); } res.json({ current: currentConfig, savedConfigs: configs || [], presets: SECURITY_PRESETS }); } ); } catch (error) { logger.error('Error reading current security headers:', error); res.status(500).json({ error: 'Failed to read current configuration' }); } }); // Get security header recommendations router.get('/recommendations', authenticate, requirePermission('security.view_audit'), readLimiter, async (req, res) => { try { const recommendations = await generateSecurityRecommendations(); res.json(recommendations); } catch (error) { logger.error('Error generating recommendations:', error); res.status(500).json({ error: 'Failed to generate recommendations' }); } }); // Test security headers router.post('/test', authenticate, requirePermission('security.manage'), modifyLimiter, async (req, res) => { try { const { config } = req.body; if (!config) { return res.status(400).json({ error: 'Configuration required' }); } const testResults = await testSecurityConfiguration(config); res.json(testResults); } catch (error) { logger.error('Error testing security headers:', error); res.status(500).json({ error: 'Failed to test configuration' }); } }); // Save security headers configuration router.post('/save', authenticate, requirePermission('security.manage'), modifyLimiter, async (req, res) => { try { const { configName, config, setActive } = req.body; if (!configName || !config) { return res.status(400).json({ error: 'Configuration name and config required' }); } // If setting as active, deactivate all others first if (setActive) { await new Promise((resolve, reject) => { db.run('UPDATE security_headers_config SET is_active = 0', [], (err) => { if (err) reject(err); else resolve(); }); }); } // Insert new configuration const stmt = db.prepare(` INSERT INTO security_headers_config ( config_name, csp_default_src, csp_script_src, csp_style_src, csp_img_src, csp_media_src, csp_connect_src, csp_font_src, csp_frame_src, csp_object_src, csp_base_uri, csp_form_action, csp_frame_ancestors, hsts_enabled, hsts_max_age, hsts_include_subdomains, hsts_preload, referrer_policy, x_content_type_options, x_frame_options, x_xss_protection, is_active, created_by ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `); stmt.run( configName, config.csp_default_src, config.csp_script_src, config.csp_style_src, config.csp_img_src, config.csp_media_src, config.csp_connect_src, config.csp_font_src, config.csp_frame_src, config.csp_object_src, config.csp_base_uri, config.csp_form_action, config.csp_frame_ancestors, config.hsts_enabled ? 1 : 0, config.hsts_max_age, config.hsts_include_subdomains ? 1 : 0, config.hsts_preload ? 1 : 0, config.referrer_policy, config.x_content_type_options ? 1 : 0, config.x_frame_options, config.x_xss_protection ? 1 : 0, setActive ? 1 : 0, req.user.id, function(err) { if (err) { logger.error('Error saving security headers config:', err); return res.status(500).json({ error: 'Failed to save configuration' }); } // Log to history db.run( `INSERT INTO security_headers_history (config_id, action, new_config, changed_by) VALUES (?, ?, ?, ?)`, [this.lastID, 'created', JSON.stringify(config), req.user.id] ); logger.info(`Security headers configuration '${configName}' saved by user ${req.user.id}`); res.json({ success: true, configId: this.lastID, message: 'Configuration saved successfully' }); } ); } catch (error) { logger.error('Error saving security headers:', error); res.status(500).json({ error: 'Failed to save configuration' }); } }); // Apply security headers configuration (updates server.js) router.post('/apply/:configId', authenticate, requirePermission('security.manage'), modifyLimiter, async (req, res) => { try { const { configId } = req.params; // Get configuration db.get( 'SELECT * FROM security_headers_config WHERE id = ?', [configId], async (err, config) => { if (err || !config) { return res.status(404).json({ error: 'Configuration not found' }); } try { // Backup current server.js const serverPath = path.join(__dirname, '../server.js'); const backupPath = path.join(__dirname, '../server.js.backup'); await fs.copyFile(serverPath, backupPath); // Note: Applying configuration requires server restart // This endpoint saves the config as active but warns user to restart await new Promise((resolve, reject) => { db.run('UPDATE security_headers_config SET is_active = 0', [], (err) => { if (err) reject(err); else resolve(); }); }); await new Promise((resolve, reject) => { db.run( 'UPDATE security_headers_config SET is_active = 1, updated_at = CURRENT_TIMESTAMP WHERE id = ?', [configId], (err) => { if (err) reject(err); else resolve(); } ); }); // Log to history db.run( `INSERT INTO security_headers_history (config_id, action, new_config, changed_by) VALUES (?, ?, ?, ?)`, [configId, 'applied', JSON.stringify(config), req.user.id] ); logger.info(`Security headers configuration ${configId} marked as active by user ${req.user.id}`); res.json({ success: true, warning: 'Configuration saved. Server restart required to apply changes.', requiresRestart: true }); } catch (error) { logger.error('Error applying security headers:', error); res.status(500).json({ error: 'Failed to apply configuration' }); } } ); } catch (error) { logger.error('Error in apply endpoint:', error); res.status(500).json({ error: 'Failed to apply configuration' }); } }); // Get configuration history router.get('/history', authenticate, requirePermission('security.view_audit'), readLimiter, async (req, res) => { try { db.all( `SELECT h.*, u.username, c.config_name FROM security_headers_history h LEFT JOIN users u ON h.changed_by = u.id LEFT JOIN security_headers_config c ON h.config_id = c.id ORDER BY h.timestamp DESC LIMIT 50`, [], (err, history) => { if (err) { logger.error('Error fetching security headers history:', err); return res.status(500).json({ error: 'Failed to fetch history' }); } res.json(history || []); } ); } catch (error) { logger.error('Error fetching history:', error); res.status(500).json({ error: 'Failed to fetch history' }); } }); // Delete saved configuration router.delete('/:configId', authenticate, requirePermission('security.manage'), modifyLimiter, async (req, res) => { try { const { configId } = req.params; db.get('SELECT is_active FROM security_headers_config WHERE id = ?', [configId], (err, config) => { if (err) { return res.status(500).json({ error: 'Failed to check configuration' }); } if (!config) { return res.status(404).json({ error: 'Configuration not found' }); } if (config.is_active) { return res.status(400).json({ error: 'Cannot delete active configuration' }); } db.run('DELETE FROM security_headers_config WHERE id = ?', [configId], (err) => { if (err) { logger.error('Error deleting security headers config:', err); return res.status(500).json({ error: 'Failed to delete configuration' }); } logger.info(`Security headers configuration ${configId} deleted by user ${req.user.id}`); res.json({ success: true, message: 'Configuration deleted' }); }); }); } catch (error) { logger.error('Error deleting configuration:', error); res.status(500).json({ error: 'Failed to delete configuration' }); } }); // Helper functions function extractCSPFromCode(serverCode) { // This is a simplified extraction - in production, you'd parse more carefully return { defaultSrc: ["'self'"], scriptSrc: ["'self'", "'unsafe-inline'", "'unsafe-eval'"], styleSrc: ["'self'", "'unsafe-inline'"], imgSrc: ["'self'", "data:", "blob:", "https:", "http:"], mediaSrc: ["'self'", "blob:", "data:", "mediastream:", "https:", "http:", "*"], connectSrc: ["'self'", "https:", "http:", "ws:", "wss:", "blob:", "*"], fontSrc: ["'self'", "data:"], frameSrc: ["'self'"], objectSrc: ["'none'"], baseUri: ["'self'"], formAction: ["'self'"], frameAncestors: ["'self'"] }; } async function generateSecurityRecommendations() { const recommendations = []; let score = 100; // Check current environment const isProduction = process.env.NODE_ENV === 'production'; if (!isProduction) { recommendations.push({ severity: 'info', category: 'Environment', title: 'Development Mode Active', description: 'CSP is in report-only mode. Some security headers are disabled.', action: 'Deploy to production to enable full security', impact: 'low' }); } // Check for CSP violations const violationCount = await new Promise((resolve) => { db.get( 'SELECT COUNT(*) as count FROM csp_violations WHERE created_at > datetime("now", "-7 days")', [], (err, row) => resolve(row?.count || 0) ); }); if (violationCount > 10) { score -= 10; recommendations.push({ severity: 'warning', category: 'CSP', title: `${violationCount} CSP Violations in Last 7 Days`, description: `Your Content Security Policy is being violated ${violationCount} times, indicating resources are being blocked or attempted to load from unauthorized sources. Common causes: (1) External scripts/styles not whitelisted in CSP, (2) Inline event handlers (onclick, onload, etc.), (3) Third-party widgets or ads, (4) Browser extensions injecting content, (5) Misconfigured CDN URLs. This could indicate attempted attacks or legitimate resources being blocked.`, action: 'Visit /security/csp dashboard to analyze violations by: (1) Violated Directive - identify which CSP rule is being broken, (2) Blocked URI - see what resources are blocked, (3) Source File - find where the violation originates. Then either: (a) Add legitimate sources to your CSP whitelist, (b) Remove inline scripts/handlers, (c) Block malicious sources, (d) Update third-party library configurations.', impact: 'medium', details: { threshold: 'More than 10 violations may indicate policy misconfiguration', monitoring: 'Check CSP Dashboard for patterns and trends', action: 'Use Statistics tab to group violations and identify root causes' } }); } // Check for unsafe CSP directives recommendations.push({ severity: 'warning', category: 'CSP', title: 'Unsafe CSP Directives Detected', description: "Your CSP includes 'unsafe-inline' and 'unsafe-eval' in script-src, which weakens XSS (Cross-Site Scripting) protection. These directives allow inline JavaScript and dynamic code evaluation, making it easier for attackers to inject malicious scripts if they find a vulnerability. However, for IPTV streaming apps using React/Vite, these are often necessary for: (1) React's inline scripts and hot module replacement, (2) MUI's dynamic styling, (3) Third-party streaming libraries, (4) External IPTV APIs. Your server already generates cryptographic nonces for better security.", action: 'Current configuration is acceptable for an IPTV app. To improve: (1) Monitor CSP violations regularly in the CSP Dashboard, (2) Keep input validation strict (already implemented), (3) Update dependencies frequently, (4) For future major refactoring, explore migrating to nonce-only scripts by configuring Vite to inject nonces and removing unsafe-inline. Note: Your nonce generation is already in place at server.js - you have the foundation for future improvement.', impact: 'medium', details: { currentSetup: 'Multiple defense layers active: input validation, parameterized queries, authentication, rate limiting', tradeoff: 'Security vs Functionality: Current score 85-90 is excellent for feature-rich apps', futureWork: 'Nonce-based CSP requires: Vite config changes, React hydration updates, third-party library compatibility' } }); // Check HSTS if (!isProduction) { recommendations.push({ severity: 'info', category: 'HSTS', title: 'HSTS Disabled in Development', description: 'HTTP Strict Transport Security (HSTS) forces browsers to only connect via HTTPS, preventing man-in-the-middle attacks and SSL stripping. Currently disabled in development mode to allow HTTP testing. In production, HSTS will be enabled with: max-age=31536000 (1 year), includeSubDomains, and preload flags.', action: 'For production deployment: (1) Ensure valid SSL/TLS certificate is installed, (2) Configure reverse proxy (nginx/Apache) for HTTPS, (3) Set NODE_ENV=production to enable HSTS, (4) Test HTTPS functionality before enabling, (5) Consider HSTS preload list submission at hstspreload.org for maximum security (permanent, cannot be undone easily).', impact: 'low', details: { currentMode: 'Development - HSTS off to allow HTTP testing', productionMode: 'HSTS enabled automatically with secure settings', preloadWarning: 'HSTS preload is permanent - only enable after thorough HTTPS testing' } }); } // Add positive recommendation if security is good if (recommendations.length === 0 || (recommendations.length === 1 && recommendations[0].severity === 'info')) { recommendations.push({ severity: 'info', category: 'Security', title: 'Excellent Security Configuration', description: 'Your security headers are well-configured with minimal issues detected. All critical protections are active: CSP for XSS prevention, HSTS for HTTPS enforcement (in production), X-Content-Type-Options for MIME sniffing protection, X-Frame-Options for clickjacking prevention, and proper referrer policy.', action: 'Maintain current configuration and continue monitoring: (1) Review CSP violations weekly, (2) Keep dependencies updated with npm audit, (3) Monitor security audit logs for suspicious activity, (4) Backup security configurations before changes.', impact: 'low', details: { score: 'Grade A security for IPTV streaming application', maintenance: 'Regular monitoring and updates recommended', compliance: 'Meets OWASP security standards' } }); } // Security score calculation if (violationCount > 10) score -= 10; if (violationCount > 50) score -= 15; return { score: Math.max(0, score), grade: score >= 90 ? 'A' : score >= 80 ? 'B' : score >= 70 ? 'C' : score >= 60 ? 'D' : 'F', recommendations: recommendations, summary: { total: recommendations.length, critical: recommendations.filter(r => r.severity === 'error').length, warnings: recommendations.filter(r => r.severity === 'warning').length, info: recommendations.filter(r => r.severity === 'info').length } }; } async function testSecurityConfiguration(config) { const results = { passed: [], warnings: [], errors: [], score: 100 }; // Test CSP strictness if (config.csp_script_src && config.csp_script_src.includes("'unsafe-eval'")) { results.warnings.push({ test: 'CSP Script Evaluation', message: "'unsafe-eval' allows dynamic code execution, reducing XSS protection" }); results.score -= 5; } else { results.passed.push({ test: 'CSP Script Evaluation', message: 'No unsafe-eval in script-src' }); } if (config.csp_script_src && config.csp_script_src.includes("'unsafe-inline'")) { results.warnings.push({ test: 'CSP Inline Scripts', message: "'unsafe-inline' allows inline scripts, reducing XSS protection" }); results.score -= 5; } else { results.passed.push({ test: 'CSP Inline Scripts', message: 'No unsafe-inline in script-src' }); } // Test object-src if (config.csp_object_src && config.csp_object_src.includes("'none'")) { results.passed.push({ test: 'Plugin Blocking', message: 'object-src is none - plugins blocked' }); } else { results.warnings.push({ test: 'Plugin Blocking', message: 'Consider setting object-src to none to block plugins' }); results.score -= 5; } // Test HSTS if (config.hsts_enabled) { if (config.hsts_max_age >= 31536000) { results.passed.push({ test: 'HSTS Duration', message: 'HSTS max-age is 1 year or more' }); } else { results.warnings.push({ test: 'HSTS Duration', message: 'HSTS max-age should be at least 1 year (31536000 seconds)' }); results.score -= 5; } if (config.hsts_include_subdomains) { results.passed.push({ test: 'HSTS Subdomains', message: 'HSTS includeSubDomains is enabled' }); } } else { results.errors.push({ test: 'HSTS Enabled', message: 'HSTS is disabled - HTTPS enforcement is not active' }); results.score -= 15; } // Test X-Frame-Options if (config.x_frame_options === 'DENY' || config.x_frame_options === 'SAMEORIGIN') { results.passed.push({ test: 'Clickjacking Protection', message: `X-Frame-Options is set to ${config.x_frame_options}` }); } else { results.warnings.push({ test: 'Clickjacking Protection', message: 'X-Frame-Options should be DENY or SAMEORIGIN' }); results.score -= 10; } // Test X-Content-Type-Options if (config.x_content_type_options) { results.passed.push({ test: 'MIME Sniffing Protection', message: 'X-Content-Type-Options: nosniff is enabled' }); } else { results.errors.push({ test: 'MIME Sniffing Protection', message: 'X-Content-Type-Options should be enabled' }); results.score -= 10; } return { score: Math.max(0, results.score), grade: results.score >= 90 ? 'A' : results.score >= 80 ? 'B' : results.score >= 70 ? 'C' : results.score >= 60 ? 'D' : 'F', passed: results.passed, warnings: results.warnings, errors: results.errors, summary: `${results.passed.length} passed, ${results.warnings.length} warnings, ${results.errors.length} errors` }; } module.exports = router;