mirror of
https://github.com/kjanat/livedash-node.git
synced 2026-01-16 18:32:10 +01:00
- Implement repository pattern for data access layer - Add comprehensive service layer for business logic - Create scheduler management system with health monitoring - Add bounded buffer utility for memory management - Enhance security audit logging with retention policies
535 lines
14 KiB
TypeScript
535 lines
14 KiB
TypeScript
// CSP types and browser-safe utilities
|
|
// Server-only functions (generateNonce, buildCSP) are in csp-server.ts
|
|
|
|
export interface CSPConfig {
|
|
nonce?: string;
|
|
isDevelopment?: boolean;
|
|
reportUri?: string;
|
|
enforceMode?: boolean;
|
|
strictMode?: boolean;
|
|
allowedExternalDomains?: string[];
|
|
reportingLevel?: "none" | "violations" | "all";
|
|
}
|
|
|
|
/**
|
|
* Build Content Security Policy string based on configuration
|
|
*/
|
|
export function buildCSPString(config: CSPConfig = {}): string {
|
|
const {
|
|
nonce,
|
|
isDevelopment = false,
|
|
reportUri,
|
|
strictMode = false,
|
|
allowedExternalDomains = [],
|
|
} = config;
|
|
|
|
const directives: Record<string, string[]> = {
|
|
"default-src": ["'self'"],
|
|
"script-src": ["'self'"],
|
|
"style-src": ["'self'"],
|
|
"img-src": ["'self'", "data:", "blob:"],
|
|
"font-src": ["'self'", "data:"],
|
|
"connect-src": ["'self'"],
|
|
"frame-src": ["'none'"],
|
|
"object-src": ["'none'"],
|
|
"base-uri": ["'self'"],
|
|
"form-action": ["'self'"],
|
|
"frame-ancestors": ["'none'"],
|
|
"upgrade-insecure-requests": [],
|
|
};
|
|
|
|
// Script source configuration
|
|
if (isDevelopment) {
|
|
directives["script-src"].push("'unsafe-eval'", "'unsafe-inline'");
|
|
} else if (nonce) {
|
|
directives["script-src"].push(
|
|
`'nonce-${nonce}'`,
|
|
"'strict-dynamic'",
|
|
"'unsafe-inline'" // Required for browsers that don't support nonce
|
|
);
|
|
}
|
|
|
|
// Style source configuration
|
|
if (isDevelopment) {
|
|
directives["style-src"].push("'unsafe-inline'");
|
|
} else if (nonce) {
|
|
directives["style-src"].push(`'nonce-${nonce}'`);
|
|
}
|
|
|
|
// Development-specific relaxations
|
|
if (isDevelopment) {
|
|
// Allow WebSocket connections for hot reload
|
|
directives["connect-src"].push("ws:", "wss:");
|
|
// Allow local development servers
|
|
directives["connect-src"].push("http://localhost:*", "http://127.0.0.1:*");
|
|
}
|
|
|
|
// Map tile sources
|
|
directives["img-src"].push(
|
|
"https://*.basemaps.cartocdn.com",
|
|
"https://*.openstreetmap.org",
|
|
"https://unpkg.com" // For Leaflet markers
|
|
);
|
|
|
|
// External domains configuration
|
|
if (allowedExternalDomains.length > 0) {
|
|
directives["connect-src"].push(...allowedExternalDomains);
|
|
} else if (!strictMode) {
|
|
// In non-strict mode, allow HTTPS connections
|
|
directives["connect-src"].push("https:");
|
|
}
|
|
|
|
// Worker sources
|
|
directives["worker-src"] = ["'self'", "blob:"];
|
|
|
|
// Media sources
|
|
directives["media-src"] = ["'self'"];
|
|
|
|
// Manifest source
|
|
directives["manifest-src"] = ["'self'"];
|
|
|
|
// Report URI
|
|
if (reportUri) {
|
|
directives["report-uri"] = [reportUri];
|
|
directives["report-to"] = ["csp-endpoint"];
|
|
}
|
|
|
|
// Build the CSP string
|
|
return Object.entries(directives)
|
|
.filter(
|
|
([_, values]) =>
|
|
values.length > 0 ||
|
|
["upgrade-insecure-requests", "block-all-mixed-content"].includes(_)
|
|
)
|
|
.map(([directive, values]) => {
|
|
if (values.length === 0) {
|
|
return directive;
|
|
}
|
|
return `${directive} ${values.join(" ")}`;
|
|
})
|
|
.join("; ");
|
|
}
|
|
|
|
export interface CSPViolationReport {
|
|
"csp-report": {
|
|
"document-uri": string;
|
|
referrer: string;
|
|
"violated-directive": string;
|
|
"original-policy": string;
|
|
"blocked-uri": string;
|
|
"source-file"?: string;
|
|
"line-number"?: number;
|
|
"column-number"?: number;
|
|
"script-sample"?: string;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Helper function to check unsafe directives
|
|
*/
|
|
function checkUnsafeDirectives(
|
|
csp: string,
|
|
strictMode: boolean,
|
|
warnings: string[],
|
|
errors: string[],
|
|
recommendations: string[]
|
|
): number {
|
|
let scorePenalty = 0;
|
|
|
|
if (csp.includes("'unsafe-inline'") && !csp.includes("'nonce-")) {
|
|
warnings.push("Using 'unsafe-inline' without nonce is less secure");
|
|
scorePenalty += 15;
|
|
recommendations.push(
|
|
"Implement nonce-based CSP for inline scripts and styles"
|
|
);
|
|
}
|
|
|
|
if (csp.includes("'unsafe-eval'")) {
|
|
if (strictMode) {
|
|
errors.push("'unsafe-eval' is not allowed in strict mode");
|
|
scorePenalty += 25;
|
|
} else {
|
|
warnings.push("'unsafe-eval' allows dangerous code execution");
|
|
scorePenalty += 10;
|
|
}
|
|
}
|
|
|
|
return scorePenalty;
|
|
}
|
|
|
|
/**
|
|
* Helper function to check wildcard usage
|
|
*/
|
|
function checkWildcardUsage(
|
|
csp: string,
|
|
errors: string[],
|
|
recommendations: string[]
|
|
): number {
|
|
const hasProblematicWildcards =
|
|
csp.includes(" *") ||
|
|
csp.includes("*://") ||
|
|
(csp.includes("*") && !csp.includes("*.") && !csp.includes("wss: ws:"));
|
|
|
|
if (hasProblematicWildcards) {
|
|
errors.push("Wildcard (*) sources are not recommended");
|
|
recommendations.push("Replace wildcards with specific trusted domains");
|
|
return 30;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Helper function to check security features
|
|
*/
|
|
function checkSecurityFeatures(
|
|
csp: string,
|
|
warnings: string[],
|
|
recommendations: string[]
|
|
): number {
|
|
let scorePenalty = 0;
|
|
|
|
if (
|
|
csp.includes("data:") &&
|
|
!csp.includes("img-src") &&
|
|
!csp.includes("font-src")
|
|
) {
|
|
warnings.push("data: URIs should be limited to specific directives");
|
|
scorePenalty += 5;
|
|
}
|
|
|
|
if (!csp.includes("upgrade-insecure-requests")) {
|
|
warnings.push("Missing HTTPS upgrade directive");
|
|
scorePenalty += 10;
|
|
recommendations.push("Add 'upgrade-insecure-requests' directive");
|
|
}
|
|
|
|
if (!csp.includes("frame-ancestors")) {
|
|
warnings.push("Missing frame-ancestors directive");
|
|
scorePenalty += 15;
|
|
recommendations.push(
|
|
"Add 'frame-ancestors 'none'' to prevent clickjacking"
|
|
);
|
|
}
|
|
|
|
return scorePenalty;
|
|
}
|
|
|
|
/**
|
|
* Helper function to check required directives
|
|
*/
|
|
function checkRequiredDirectives(csp: string, errors: string[]): number {
|
|
const requiredDirectives = [
|
|
"default-src",
|
|
"script-src",
|
|
"style-src",
|
|
"object-src",
|
|
"base-uri",
|
|
"form-action",
|
|
];
|
|
|
|
let scorePenalty = 0;
|
|
for (const directive of requiredDirectives) {
|
|
if (!csp.includes(directive)) {
|
|
errors.push(`Missing required directive: ${directive}`);
|
|
scorePenalty += 20;
|
|
}
|
|
}
|
|
|
|
return scorePenalty;
|
|
}
|
|
|
|
/**
|
|
* Helper function to check additional features
|
|
*/
|
|
function checkAdditionalFeatures(
|
|
csp: string,
|
|
strictMode: boolean,
|
|
warnings: string[],
|
|
recommendations: string[]
|
|
): number {
|
|
let scorePenalty = 0;
|
|
|
|
if (csp.includes("'nonce-") && !csp.includes("'strict-dynamic'")) {
|
|
recommendations.push(
|
|
"Consider adding 'strict-dynamic' for better nonce-based security"
|
|
);
|
|
}
|
|
|
|
if (!csp.includes("report-uri") && !csp.includes("report-to")) {
|
|
warnings.push("Missing CSP violation reporting");
|
|
scorePenalty += 5;
|
|
recommendations.push("Add CSP violation reporting for monitoring");
|
|
}
|
|
|
|
if (strictMode) {
|
|
if (csp.includes("https:") && !csp.includes("connect-src")) {
|
|
warnings.push("Broad HTTPS allowlist detected in strict mode");
|
|
scorePenalty += 10;
|
|
recommendations.push("Replace 'https:' with specific trusted domains");
|
|
}
|
|
}
|
|
|
|
return scorePenalty;
|
|
}
|
|
|
|
/**
|
|
* Enhanced CSP validation with security best practices
|
|
*/
|
|
export function validateCSP(
|
|
csp: string,
|
|
options: { strictMode?: boolean } = {}
|
|
): {
|
|
isValid: boolean;
|
|
warnings: string[];
|
|
errors: string[];
|
|
securityScore: number;
|
|
recommendations: string[];
|
|
} {
|
|
const warnings: string[] = [];
|
|
const errors: string[] = [];
|
|
const recommendations: string[] = [];
|
|
const { strictMode = false } = options;
|
|
|
|
let securityScore = 100;
|
|
|
|
securityScore -= checkUnsafeDirectives(
|
|
csp,
|
|
strictMode,
|
|
warnings,
|
|
errors,
|
|
recommendations
|
|
);
|
|
securityScore -= checkWildcardUsage(csp, errors, recommendations);
|
|
securityScore -= checkSecurityFeatures(csp, warnings, recommendations);
|
|
securityScore -= checkRequiredDirectives(csp, errors);
|
|
securityScore -= checkAdditionalFeatures(
|
|
csp,
|
|
strictMode,
|
|
warnings,
|
|
recommendations
|
|
);
|
|
|
|
return {
|
|
isValid: errors.length === 0,
|
|
warnings,
|
|
errors,
|
|
securityScore: Math.max(0, securityScore),
|
|
recommendations,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Parse CSP violation report
|
|
*/
|
|
export function parseCSPViolation(report: CSPViolationReport): {
|
|
directive: string;
|
|
blockedUri: string;
|
|
sourceFile?: string;
|
|
lineNumber?: number;
|
|
isInlineViolation: boolean;
|
|
isCritical: boolean;
|
|
} {
|
|
const cspReport = report["csp-report"];
|
|
|
|
const isInlineViolation =
|
|
cspReport["blocked-uri"] === "inline" ||
|
|
cspReport["blocked-uri"] === "eval";
|
|
|
|
const isCritical =
|
|
cspReport["violated-directive"].startsWith("script-src") ||
|
|
cspReport["violated-directive"].startsWith("object-src");
|
|
|
|
return {
|
|
directive: cspReport["violated-directive"],
|
|
blockedUri: cspReport["blocked-uri"],
|
|
sourceFile: cspReport["source-file"],
|
|
lineNumber: cspReport["line-number"],
|
|
isInlineViolation,
|
|
isCritical,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* CSP bypass detection patterns - optimized to prevent ReDoS attacks
|
|
*/
|
|
export const CSP_BYPASS_PATTERNS = [
|
|
// Common XSS bypass attempts (exact matches to prevent ReDoS)
|
|
/^javascript:/i,
|
|
/^data:text\/html/i,
|
|
/^vbscript:/i,
|
|
/^livescript:/i,
|
|
|
|
// Base64 encoded attempts (limited quantifiers to prevent ReDoS)
|
|
/^data:[^;]{0,50};base64[^,]{0,100},.*script/i,
|
|
/^data:text\/javascript/i,
|
|
/^data:application\/javascript/i,
|
|
|
|
// JSONP callback manipulation (limited lookahead)
|
|
/callback=[^&]{0,200}script/i,
|
|
|
|
// Common CSP bypass techniques (limited quantifiers)
|
|
/location\.href[^;]{0,100}javascript/i,
|
|
/document\.write[^;]{0,100}script/i,
|
|
/\beval\s*\(/i,
|
|
/\bnew\s+Function\s*\(/i,
|
|
/setTimeout\s*\(\s*['"`][^'"`]{0,500}['"`]/i,
|
|
/setInterval\s*\(\s*['"`][^'"`]{0,500}['"`]/i,
|
|
];
|
|
|
|
/**
|
|
* Test CSP implementation with common scenarios
|
|
*/
|
|
export function testCSPImplementation(csp: string): {
|
|
testResults: Array<{
|
|
name: string;
|
|
passed: boolean;
|
|
description: string;
|
|
recommendation?: string;
|
|
}>;
|
|
overallScore: number;
|
|
} {
|
|
const testResults: Array<{
|
|
name: string;
|
|
passed: boolean;
|
|
description: string;
|
|
recommendation?: string;
|
|
}> = [];
|
|
|
|
// Test 1: Script injection protection
|
|
testResults.push({
|
|
name: "Script Injection Protection",
|
|
passed: !csp.includes("'unsafe-inline'") || csp.includes("'nonce-"),
|
|
description: "Checks if inline scripts are properly controlled",
|
|
recommendation:
|
|
csp.includes("'unsafe-inline'") && !csp.includes("'nonce-")
|
|
? "Use nonce-based CSP instead of 'unsafe-inline'"
|
|
: undefined,
|
|
});
|
|
|
|
// Test 2: Eval protection
|
|
testResults.push({
|
|
name: "Eval Protection",
|
|
passed: !csp.includes("'unsafe-eval'"),
|
|
description: "Ensures eval() and similar functions are blocked",
|
|
recommendation: csp.includes("'unsafe-eval'")
|
|
? "Remove 'unsafe-eval' to prevent code injection"
|
|
: undefined,
|
|
});
|
|
|
|
// Test 3: Object blocking
|
|
testResults.push({
|
|
name: "Object Blocking",
|
|
passed: csp.includes("object-src 'none'"),
|
|
description: "Blocks dangerous object, embed, and applet elements",
|
|
recommendation: !csp.includes("object-src 'none'")
|
|
? "Add 'object-src 'none'' to block plugins"
|
|
: undefined,
|
|
});
|
|
|
|
// Test 4: Frame protection
|
|
testResults.push({
|
|
name: "Frame Protection",
|
|
passed:
|
|
csp.includes("frame-ancestors 'none'") ||
|
|
csp.includes("frame-ancestors 'self'"),
|
|
description: "Prevents clickjacking attacks",
|
|
recommendation: !csp.includes("frame-ancestors")
|
|
? "Add 'frame-ancestors 'none'' for clickjacking protection"
|
|
: undefined,
|
|
});
|
|
|
|
// Test 5: HTTPS enforcement
|
|
testResults.push({
|
|
name: "HTTPS Enforcement",
|
|
passed: csp.includes("upgrade-insecure-requests"),
|
|
description: "Automatically upgrades HTTP requests to HTTPS",
|
|
recommendation: !csp.includes("upgrade-insecure-requests")
|
|
? "Add 'upgrade-insecure-requests' for automatic HTTPS"
|
|
: undefined,
|
|
});
|
|
|
|
// Test 6: Base URI restriction
|
|
testResults.push({
|
|
name: "Base URI Restriction",
|
|
passed: csp.includes("base-uri 'self'") || csp.includes("base-uri 'none'"),
|
|
description: "Prevents base tag injection attacks",
|
|
recommendation: !csp.includes("base-uri")
|
|
? "Add 'base-uri 'self'' to prevent base tag attacks"
|
|
: undefined,
|
|
});
|
|
|
|
// Test 7: Form action restriction
|
|
testResults.push({
|
|
name: "Form Action Restriction",
|
|
passed: csp.includes("form-action 'self'") || csp.includes("form-action"),
|
|
description: "Controls where forms can be submitted",
|
|
recommendation: !csp.includes("form-action")
|
|
? "Add 'form-action 'self'' to control form submissions"
|
|
: undefined,
|
|
});
|
|
|
|
// Test 8: Reporting configuration
|
|
testResults.push({
|
|
name: "Violation Reporting",
|
|
passed: csp.includes("report-uri") || csp.includes("report-to"),
|
|
description: "Enables CSP violation monitoring",
|
|
recommendation:
|
|
!csp.includes("report-uri") && !csp.includes("report-to")
|
|
? "Add 'report-uri' for violation monitoring"
|
|
: undefined,
|
|
});
|
|
|
|
const passedTests = testResults.filter((test) => test.passed).length;
|
|
const overallScore = Math.round((passedTests / testResults.length) * 100);
|
|
|
|
return {
|
|
testResults,
|
|
overallScore,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Detect potential CSP bypass attempts
|
|
*/
|
|
export function detectCSPBypass(content: string): {
|
|
isDetected: boolean;
|
|
patterns: string[];
|
|
riskLevel: "low" | "medium" | "high";
|
|
} {
|
|
const detectedPatterns: string[] = [];
|
|
|
|
for (const pattern of CSP_BYPASS_PATTERNS) {
|
|
if (pattern.test(content)) {
|
|
detectedPatterns.push(pattern.source);
|
|
}
|
|
}
|
|
|
|
// Determine risk level based on pattern types (ReDoS-safe patterns)
|
|
const highRiskPatterns = [
|
|
/^javascript:/i,
|
|
/\beval\s*\(/i,
|
|
/\bnew\s+Function\s*\(/i,
|
|
/^data:text\/javascript/i,
|
|
/^data:application\/javascript/i,
|
|
/^data:[^;]{0,50};base64[^,]{0,100},.*script/i,
|
|
];
|
|
|
|
const hasHighRiskPattern = detectedPatterns.some((pattern) =>
|
|
highRiskPatterns.some((highRisk) => highRisk.source === pattern)
|
|
);
|
|
|
|
const riskLevel =
|
|
hasHighRiskPattern || detectedPatterns.length >= 3
|
|
? "high"
|
|
: detectedPatterns.length >= 1
|
|
? "medium"
|
|
: "low";
|
|
|
|
return {
|
|
isDetected: detectedPatterns.length > 0,
|
|
patterns: detectedPatterns,
|
|
riskLevel,
|
|
};
|
|
}
|