Initial commit: StreamFlow IPTV platform
This commit is contained in:
commit
73a8ae9ffd
1240 changed files with 278451 additions and 0 deletions
679
backend/routes/security-headers.js
Normal file
679
backend/routes/security-headers.js
Normal file
|
|
@ -0,0 +1,679 @@
|
|||
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;
|
||||
Loading…
Add table
Add a link
Reference in a new issue