Files
livedash-node/scripts/validate-csp-implementation.ts
Kaj Kowalski 1eea2cc3e4 refactor: fix biome linting issues and update project documentation
- Fix 36+ biome linting issues reducing errors/warnings from 227 to 191
- Replace explicit 'any' types with proper TypeScript interfaces
- Fix React hooks dependencies and useCallback patterns
- Resolve unused variables and parameter assignment issues
- Improve accessibility with proper label associations
- Add comprehensive API documentation for admin and security features
- Update README.md with accurate PostgreSQL setup and current tech stack
- Create complete documentation for audit logging, CSP monitoring, and batch processing
- Fix outdated project information and missing developer workflows
2025-07-12 00:28:09 +02:00

480 lines
13 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env tsx
import {
buildCSP,
validateCSP,
generateNonce,
detectCSPBypass,
testCSPImplementation,
} from "../lib/csp";
interface CSPTestResult {
test: string;
passed: boolean;
details?: string;
recommendations?: string[];
}
class CSPValidator {
private results: CSPTestResult[] = [];
private addResult(
test: string,
passed: boolean,
details?: string,
recommendations?: string[]
) {
this.results.push({ test, passed, details, recommendations });
}
async validateNonceGeneration() {
console.log("🎲 Testing Nonce Generation...");
// Test uniqueness
const nonces = new Set();
const iterations = 1000;
let duplicateFound = false;
for (let i = 0; i < iterations; i++) {
const nonce = generateNonce();
if (nonces.has(nonce)) {
duplicateFound = true;
break;
}
nonces.add(nonce);
}
this.addResult(
"Nonce Uniqueness",
!duplicateFound,
duplicateFound
? "Duplicate nonce detected"
: `${iterations} unique nonces generated`,
duplicateFound
? ["Check entropy source", "Verify crypto.randomBytes"]
: undefined
);
// Test format
const testNonce = generateNonce();
const validFormat = /^[A-Za-z0-9+/]+=*$/.test(testNonce);
this.addResult(
"Nonce Format",
validFormat,
`Generated nonce: ${testNonce}`,
!validFormat ? ["Ensure proper base64 encoding"] : undefined
);
// Test length
const decodedLength = Buffer.from(testNonce, "base64").length;
const correctLength = decodedLength === 16;
this.addResult(
"Nonce Length",
correctLength,
`Decoded length: ${decodedLength} bytes`,
!correctLength ? ["Use 16 bytes (128 bits) for security"] : undefined
);
}
async validateProductionCSP() {
console.log("🛡️ Testing Production CSP...");
const nonce = generateNonce();
const productionCSP = buildCSP({
nonce,
isDevelopment: false,
reportUri: "/api/csp-report",
enforceMode: true,
});
console.log("Production CSP:", productionCSP);
// Validate overall structure
const validation = validateCSP(productionCSP);
this.addResult(
"CSP Validation",
validation.isValid,
`Errors: ${validation.errors.length}, Warnings: ${validation.warnings.length}`,
validation.errors.length > 0 ? validation.errors : undefined
);
// Check for secure directives
const securityTests = [
{
name: "No unsafe-inline in scripts",
test:
!productionCSP.includes("script-src") ||
!productionCSP.match(/script-src[^;]*'unsafe-inline'/),
critical: true,
},
{
name: "No unsafe-eval in scripts",
test: !productionCSP.includes("'unsafe-eval'"),
critical: true,
},
{
name: "Nonce-based script execution",
test: productionCSP.includes(`'nonce-${nonce}'`),
critical: true,
},
{
name: "Strict dynamic enabled",
test: productionCSP.includes("'strict-dynamic'"),
critical: false,
},
{
name: "Object sources blocked",
test: productionCSP.includes("object-src 'none'"),
critical: true,
},
{
name: "Base URI restricted",
test: productionCSP.includes("base-uri 'self'"),
critical: true,
},
{
name: "Frame ancestors blocked",
test: productionCSP.includes("frame-ancestors 'none'"),
critical: true,
},
{
name: "HTTPS upgrade enabled",
test: productionCSP.includes("upgrade-insecure-requests"),
critical: false,
},
{
name: "Report URI configured",
test: productionCSP.includes("report-uri /api/csp-report"),
critical: false,
},
];
for (const secTest of securityTests) {
this.addResult(
secTest.name,
secTest.test,
undefined,
!secTest.test && secTest.critical
? ["This is a critical security requirement"]
: undefined
);
}
}
async validateDevelopmentCSP() {
console.log("🔧 Testing Development CSP...");
const devCSP = buildCSP({
isDevelopment: true,
reportUri: "/api/csp-report",
});
console.log("Development CSP:", devCSP);
// Development should be more permissive but still secure
const devTests = [
{
name: "Allows unsafe-eval for dev tools",
test: devCSP.includes("'unsafe-eval'"),
},
{
name: "Allows unsafe-inline for hot reload",
test: devCSP.includes("'unsafe-inline'"),
},
{
name: "Allows WebSocket connections",
test: devCSP.includes("wss:") || devCSP.includes("ws:"),
},
{
name: "Still blocks objects",
test: devCSP.includes("object-src 'none'"),
},
{
name: "Still restricts base URI",
test: devCSP.includes("base-uri 'self'"),
},
];
for (const devTest of devTests) {
this.addResult(devTest.name, devTest.test);
}
}
async validateBypassDetection() {
console.log("🕵️ Testing Bypass Detection...");
const bypassTests = [
{
name: "Detects javascript: protocol",
content: "window.location.href = 'javascript:alert(1)'",
shouldDetect: true,
},
{
name: "Detects data: HTML injection",
content: "iframe.src = 'data:text/html,<script>alert(1)</script>'",
shouldDetect: true,
},
{
name: "Detects eval injection",
content: "eval('malicious code')",
shouldDetect: true,
},
{
name: "Detects Function constructor",
content: "new Function('alert(1)')()",
shouldDetect: true,
},
{
name: "Detects setTimeout string",
content: "setTimeout('alert(1)', 1000)",
shouldDetect: true,
},
{
name: "Ignores legitimate content",
content: "This is normal text with no dangerous patterns",
shouldDetect: false,
},
{
name: "Ignores safe JavaScript",
content: "function safeFunction() { return 'hello'; }",
shouldDetect: false,
},
];
for (const bypassTest of bypassTests) {
const detection = detectCSPBypass(bypassTest.content);
const passed = detection.isDetected === bypassTest.shouldDetect;
this.addResult(
bypassTest.name,
passed,
`Detected: ${detection.isDetected}, Risk: ${detection.riskLevel}`,
!passed ? ["Review bypass detection patterns"] : undefined
);
}
}
async validateContentSources() {
console.log("🌐 Testing Content Source Restrictions...");
const nonce = generateNonce();
const csp = buildCSP({
nonce,
isDevelopment: false,
reportUri: "/api/csp-report",
});
// Check specific content source restrictions
const sourceTests = [
{
name: "Script sources are restrictive",
test: () => {
const scriptMatch = csp.match(/script-src ([^;]+)/);
if (!scriptMatch) return false;
const sources = scriptMatch[1];
return (
sources.includes("'self'") &&
sources.includes(`'nonce-${nonce}'`) &&
!sources.includes("'unsafe-inline'") &&
!sources.includes("*")
);
},
},
{
name: "Style sources use nonce",
test: () => {
const styleMatch = csp.match(/style-src ([^;]+)/);
if (!styleMatch) return false;
const sources = styleMatch[1];
return (
sources.includes("'self'") && sources.includes(`'nonce-${nonce}'`)
);
},
},
{
name: "Image sources are limited",
test: () => {
const imgMatch = csp.match(/img-src ([^;]+)/);
if (!imgMatch) return false;
const sources = imgMatch[1];
return (
sources.includes("'self'") &&
sources.includes("data:") &&
!sources.includes("*")
);
},
},
{
name: "Connect sources are specific",
test: () => {
const connectMatch = csp.match(/connect-src ([^;]+)/);
if (!connectMatch) return false;
const sources = connectMatch[1];
return (
sources.includes("'self'") &&
sources.includes("https://api.openai.com") &&
!sources.includes("ws:") &&
!sources.includes("wss:")
);
},
},
{
name: "Font sources are restricted",
test: () => {
const fontMatch = csp.match(/font-src ([^;]+)/);
if (!fontMatch) return false;
const sources = fontMatch[1];
return (
sources.includes("'self'") &&
sources.includes("data:") &&
!sources.includes("*")
);
},
},
];
for (const sourceTest of sourceTests) {
this.addResult(
sourceTest.name,
sourceTest.test(),
undefined,
!sourceTest.test()
? ["Review and tighten content source restrictions"]
: undefined
);
}
}
async validateCompatibility() {
console.log("🔄 Testing Framework Compatibility...");
// Test that CSP works with Next.js requirements
const compatibilityTests = [
{
name: "Next.js development compatibility",
test: () => {
const devCSP = buildCSP({ isDevelopment: true });
return devCSP.includes("'unsafe-eval'"); // Required for Next.js dev
},
},
{
name: "TailwindCSS compatibility",
test: () => {
const csp = buildCSP({ isDevelopment: false });
// Should either have nonce or unsafe-inline for styles
return (
csp.includes("'nonce-") ||
csp.includes("style-src 'self' 'unsafe-inline'")
);
},
},
{
name: "JSON-LD support",
test: () => {
const nonce = generateNonce();
const csp = buildCSP({ nonce, isDevelopment: false });
// Should allow nonce-based inline scripts
return csp.includes(`'nonce-${nonce}'`);
},
},
];
for (const compatTest of compatibilityTests) {
this.addResult(
compatTest.name,
compatTest.test(),
undefined,
!compatTest.test() ? ["Ensure framework compatibility"] : undefined
);
}
}
generateReport() {
console.log("\n📊 CSP Validation Report");
console.log("=".repeat(50));
const passed = this.results.filter((r) => r.passed).length;
const failed = this.results.filter((r) => r.passed === false).length;
const critical = this.results.filter(
(r) =>
!r.passed && r.recommendations?.some((rec) => rec.includes("critical"))
).length;
console.log(`\n📈 Summary: ${passed} passed, ${failed} failed`);
if (critical > 0) {
console.log(`⚠️ Critical issues: ${critical}`);
}
console.log("\n📋 Detailed Results:");
for (const result of this.results) {
const status = result.passed ? "✅" : "❌";
console.log(`${status} ${result.test}`);
if (result.details) {
console.log(` ${result.details}`);
}
if (result.recommendations) {
for (const rec of result.recommendations) {
console.log(` 💡 ${rec}`);
}
}
}
// Security score
const securityScore = Math.round((passed / this.results.length) * 100);
console.log(`\n🛡 Security Score: ${securityScore}%`);
if (securityScore >= 90) {
console.log("🎉 Excellent CSP implementation!");
} else if (securityScore >= 80) {
console.log("🔧 Good CSP implementation with room for improvement");
} else if (securityScore >= 70) {
console.log("⚠️ CSP implementation needs attention");
} else {
console.log("🚨 CSP implementation has serious security issues");
}
return {
passed,
failed,
critical,
securityScore,
success: failed === 0 && critical === 0,
};
}
async run() {
console.log("🔒 Enhanced CSP Implementation Validation");
console.log("=".repeat(50));
await this.validateNonceGeneration();
await this.validateProductionCSP();
await this.validateDevelopmentCSP();
await this.validateBypassDetection();
await this.validateContentSources();
await this.validateCompatibility();
return this.generateReport();
}
}
// Run validation if this script is called directly
if (import.meta.url === `file://${process.argv[1]}`) {
const validator = new CSPValidator();
validator
.run()
.then((report) => {
if (!report.success) {
process.exit(1);
}
})
.catch((error) => {
console.error("❌ Validation failed:", error);
process.exit(1);
});
}