Back to blog
SecurityJanuary 1, 2024· 12 min read

Content Security Policy (CSP) Deep Dive: A Developer's Guide

A practical Fusebox guide to content security policy (csp) deep dive.

Content Security Policy (CSP) Deep Dive: A Developer's Guide

Content Security Policy is your website's bouncer—it decides what scripts, styles, and resources can execute. When properly configured, CSP can prevent XSS attacks, data theft, and other content injection vulnerabilities. Yet studies show that 95% of CSP implementations contain bypasses. This guide will help you implement CSP correctly and avoid common pitfalls.

Why CSP Matters

In 2018, British Airways suffered a Magecart attack where malicious JavaScript stole credit card details from 380,000 customers. A properly configured CSP would have prevented this attack entirely. CSP isn't just another security header—it's your last line of defense against XSS when other measures fail.

CSP Analyzer and Generator

Comprehensive CSP analysis and generation tool:

// CSP Analyzer and Generator
const CSPAnalyzer = {
    // Analyze current CSP
    analyzeCurrentCSP() {
        const analysis = {
            hasCSP: false,
            policies: [],
            issues: [],
            score: 0,
            recommendations: []
        };
        
        // Check meta tag CSP
        const metaCSP = document.querySelector('meta[http-equiv="Content-Security-Policy"]');
        if (metaCSP) {
            analysis.hasCSP = true;
            analysis.policies.push({
                source: 'meta-tag',
                policy: metaCSP.getAttribute('content')
            });
        }
        
        // Parse and analyze policies
        analysis.policies.forEach(policyObj => {
            const parsed = this.parseCSP(policyObj.policy);
            policyObj.parsed = parsed;
            policyObj.analysis = this.analyzeParsedCSP(parsed);
            
            // Aggregate issues
            analysis.issues.push(...policyObj.analysis.issues);
        });
        
        // Calculate score
        analysis.score = this.calculateCSPScore(analysis);
        
        // Generate recommendations
        analysis.recommendations = this.generateRecommendations(analysis);
        
        return analysis;
    },
    
    // Parse CSP string
    parseCSP(cspString) {
        const directives = {};
        const parts = cspString.split(';').map(p => p.trim()).filter(p => p);
        
        parts.forEach(part => {
            const [directive, ...values] = part.split(/\s+/);
            if (directive) {
                directives[directive] = values;
            }
        });
        
        return directives;
    },
    
    // Analyze parsed CSP
    analyzeParsedCSP(parsed) {
        const analysis = {
            issues: [],
            strengths: [],
            directives: {}
        };
        
        // Check for unsafe directives
        Object.entries(parsed).forEach(([directive, values]) => {
            analysis.directives[directive] = {
                values,
                issues: [],
                recommendations: []
            };
            
            // Check for unsafe-inline
            if (values.includes("'unsafe-inline'")) {
                analysis.issues.push({
                    severity: 'high',
                    directive,
                    issue: 'unsafe-inline allows inline scripts/styles',
                    impact: 'Defeats main purpose of CSP'
                });
            }
            
            // Check for unsafe-eval
            if (values.includes("'unsafe-eval'")) {
                analysis.issues.push({
                    severity: 'high',
                    directive,
                    issue: 'unsafe-eval allows eval() and similar',
                    impact: 'Major XSS risk'
                });
            }
            
            // Check for wildcards
            if (values.includes('*')) {
                analysis.issues.push({
                    severity: 'medium',
                    directive,
                    issue: 'Wildcard (*) is too permissive',
                    impact: 'Allows loading from any source'
                });
            }
            
            // Check for data: URIs
            if (values.includes('data:')) {
                analysis.issues.push({
                    severity: 'medium',
                    directive,
                    issue: 'data: URIs can be used for XSS',
                    impact: 'Potential script injection vector'
                });
            }
            
            // Check for http:// in HTTPS context
            if (window.location.protocol === 'https:' && 
                values.some(v => v.startsWith('http://'))) {
                analysis.issues.push({
                    severity: 'medium',
                    directive,
                    issue: 'HTTP sources in HTTPS context',
                    impact: 'Mixed content issues'
                });
            }
        });
        
        // Check for missing important directives
        const important = ['default-src', 'script-src', 'object-src'];
        important.forEach(directive => {
            if (!parsed[directive]) {
                analysis.issues.push({
                    severity: 'medium',
                    directive,
                    issue: `Missing ${directive} directive`,
                    impact: 'Falls back to default-src or allows all'
                });
            }
        });
        
        // Check for good practices
        if (parsed['upgrade-insecure-requests']) {
            analysis.strengths.push('Upgrades HTTP to HTTPS');
        }
        
        if (parsed['block-all-mixed-content']) {
            analysis.strengths.push('Blocks all mixed content');
        }
        
        return analysis;
    },
    
    // Calculate CSP score
    calculateCSPScore(analysis) {
        let score = 100;
        
        if (!analysis.hasCSP) return 0;
        
        analysis.issues.forEach(issue => {
            switch (issue.severity) {
                case 'high':
                    score -= 25;
                    break;
                case 'medium':
                    score -= 15;
                    break;
                case 'low':
                    score -= 5;
                    break;
            }
        });
        
        return Math.max(0, score);
    },
    
    // Generate recommendations
    generateRecommendations(analysis) {
        const recommendations = [];
        
        if (!analysis.hasCSP) {
            recommendations.push({
                priority: 'critical',
                action: 'Implement Content Security Policy',
                example: this.generateStrictCSP()
            });
            return recommendations;
        }
        
        // Specific recommendations based on issues
        analysis.issues.forEach(issue => {
            if (issue.issue.includes('unsafe-inline')) {
                recommendations.push({
                    priority: 'high',
                    action: 'Remove unsafe-inline',
                    solution: 'Use nonces or hashes for inline scripts/styles',
                    example: "script-src 'nonce-{random}' 'strict-dynamic';"
                });
            }
            
            if (issue.issue.includes('unsafe-eval')) {
                recommendations.push({
                    priority: 'high',
                    action: 'Remove unsafe-eval',
                    solution: 'Refactor code to avoid eval() and Function()'
                });
            }
            
            if (issue.issue.includes('Wildcard')) {
                recommendations.push({
                    priority: 'medium',
                    action: 'Replace wildcards with specific domains',
                    solution: 'List allowed domains explicitly'
                });
            }
        });
        
        return recommendations;
    },
    
    // Generate strict CSP
    generateStrictCSP() {
        return `default-src 'none'; script-src 'self' 'nonce-{random}' 'strict-dynamic' https:; style-src 'self' 'nonce-{random}'; img-src 'self' data: https:; font-src 'self'; connect-src 'self' https:; media-src 'self'; object-src 'none'; frame-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; upgrade-insecure-requests; block-all-mixed-content;`;
    }
};

// Analyze current page
console.log(CSPAnalyzer.analyzeCurrentCSP());

CSP Violation Reporter

Monitor and analyze CSP violations:

// CSP Violation Reporter
class CSPViolationReporter {
    constructor() {
        this.violations = [];
        this.patterns = new Map();
        this.setupListener();
    }
    
    // Setup violation listener
    setupListener() {
        document.addEventListener('securitypolicyviolation', (e) => {
            this.handleViolation(e);
        });
    }
    
    // Handle CSP violation
    handleViolation(event) {
        const violation = {
            timestamp: new Date().toISOString(),
            blockedURI: event.blockedURI,
            violatedDirective: event.violatedDirective,
            effectiveDirective: event.effectiveDirective,
            originalPolicy: event.originalPolicy,
            disposition: event.disposition,
            documentURI: event.documentURI,
            referrer: event.referrer,
            statusCode: event.statusCode,
            sourceFile: event.sourceFile,
            lineNumber: event.lineNumber,
            columnNumber: event.columnNumber,
            sample: event.sample
        };
        
        this.violations.push(violation);
        this.updatePatterns(violation);
        
        // Log for debugging
        console.warn('CSP Violation:', violation);
        
        // Send to reporting endpoint (if configured)
        this.reportViolation(violation);
    }
    
    // Update violation patterns
    updatePatterns(violation) {
        const key = `${violation.violatedDirective}:${violation.blockedURI}`;
        
        if (!this.patterns.has(key)) {
            this.patterns.set(key, {
                count: 0,
                firstSeen: violation.timestamp,
                lastSeen: violation.timestamp,
                samples: []
            });
        }
        
        const pattern = this.patterns.get(key);
        pattern.count++;
        pattern.lastSeen = violation.timestamp;
        
        if (pattern.samples.length < 5 && violation.sample) {
            pattern.samples.push(violation.sample);
        }
    }
    
    // Report violation to server
    async reportViolation(violation) {
        // Check if we have a report-uri or report-to configured
        const meta = document.querySelector('meta[http-equiv="Content-Security-Policy"]');
        if (!meta) return;
        
        const policy = meta.getAttribute('content');
        const reportUri = policy.match(/report-uri\s+([^;]+)/)?.[1];
        
        if (reportUri) {
            try {
                await fetch(reportUri.trim(), {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/csp-report'
                    },
                    body: JSON.stringify({
                        'csp-report': violation
                    })
                });
            } catch (error) {
                console.error('Failed to send CSP report:', error);
            }
        }
    }
    
    // Get violation summary
    getSummary() {
        const summary = {
            totalViolations: this.violations.length,
            byDirective: {},
            bySource: {},
            patterns: Array.from(this.patterns.entries()).map(([key, data]) => ({
                pattern: key,
                ...data
            }))
        };
        
        // Group by directive
        this.violations.forEach(v => {
            const directive = v.violatedDirective;
            if (!summary.byDirective[directive]) {
                summary.byDirective[directive] = {
                    count: 0,
                    sources: new Set()
                };
            }
            summary.byDirective[directive].count++;
            summary.byDirective[directive].sources.add(v.blockedURI);
        });
        
        // Convert sets to arrays
        Object.values(summary.byDirective).forEach(data => {
            data.sources = Array.from(data.sources);
        });
        
        // Group by source
        this.violations.forEach(v => {
            const source = v.blockedURI || 'inline';
            summary.bySource[source] = (summary.bySource[source] || 0) + 1;
        });
        
        return summary;
    }
    
    // Generate CSP based on violations
    generateCSPFromViolations() {
        const policy = {
            'default-src': ["'self'"],
            'script-src': ["'self'"],
            'style-src': ["'self'"],
            'img-src': ["'self'"],
            'connect-src': ["'self'"],
            'font-src': ["'self'"],
            'object-src': ["'none'"],
            'media-src': ["'self'"],
            'frame-src': ["'none'"],
            'worker-src': ["'self'"],
            'form-action': ["'self'"],
            'frame-ancestors': ["'none'"],
            'base-uri': ["'self'"]
        };
        
        // Add sources from violations
        this.violations.forEach(v => {
            const directive = v.effectiveDirective;
            const source = v.blockedURI;
            
            if (!policy[directive]) {
                policy[directive] = ["'self'"];
            }
            
            if (source && !source.startsWith('inline') && !source.startsWith('eval')) {
                try {
                    const url = new URL(source);
                    const origin = url.origin;
                    
                    if (!policy[directive].includes(origin)) {
                        policy[directive].push(origin);
                    }
                } catch (e) {
                    // Not a valid URL, might be 'data:' or similar
                    if (!policy[directive].includes(source)) {
                        policy[directive].push(source);
                    }
                }
            } else if (source === 'inline') {
                // Suggest using nonces instead of unsafe-inline
                console.warn(`Inline ${directive} detected - use nonces instead`);
            }
        });
        
        // Generate CSP string
        const cspString = Object.entries(policy)
            .map(([directive, sources]) => `${directive} ${sources.join(' ')}`)
            .join('; ');
        
        return cspString + ';';
    }
}

// Start monitoring
const cspReporter = new CSPViolationReporter();

CSP Nonce Implementation

Implement secure inline scripts with nonces:

// CSP Nonce Manager
class CSPNonceManager {
    constructor() {
        this.nonces = new Map();
    }
    
    // Generate cryptographically secure nonce
    generateNonce() {
        const array = new Uint8Array(16);
        crypto.getRandomValues(array);
        return btoa(String.fromCharCode.apply(null, array));
    }
    
    // Apply nonce to CSP
    applyNonceToCSP(csp, nonce) {
        const updatedCSP = csp.replace(
            /script-src([^;]*)/,
            (match, sources) => {
                if (!sources.includes(`'nonce-`)) {
                    return `script-src${sources} 'nonce-${nonce}'`;
                }
                return match;
            }
        );
        
        return updatedCSP;
    }
    
    // Add nonce to script element
    addNonceToScript(script, nonce) {
        script.setAttribute('nonce', nonce);
        this.nonces.set(script, nonce);
    }
    
    // Create nonced inline script
    createNoncedScript(code) {
        const nonce = this.generateNonce();
        const script = document.createElement('script');
        script.setAttribute('nonce', nonce);
        script.textContent = code;
        
        // Update CSP meta tag
        this.updateCSPMetaTag(nonce);
        
        return { script, nonce };
    }
    
    // Update CSP meta tag with nonce
    updateCSPMetaTag(nonce) {
        let metaCSP = document.querySelector('meta[http-equiv="Content-Security-Policy"]');
        
        if (!metaCSP) {
            metaCSP = document.createElement('meta');
            metaCSP.setAttribute('http-equiv', 'Content-Security-Policy');
            document.head.appendChild(metaCSP);
        }
        
        const currentCSP = metaCSP.getAttribute('content') || "default-src 'self'; script-src 'self'";
        const updatedCSP = this.applyNonceToCSP(currentCSP, nonce);
        metaCSP.setAttribute('content', updatedCSP);
    }
    
    // Convert inline scripts to nonced scripts
    convertInlineScripts() {
        const inlineScripts = document.querySelectorAll('script:not([src]):not([nonce])');
        const converted = [];
        
        inlineScripts.forEach(script => {
            const code = script.textContent;
            const nonce = this.generateNonce();
            
            // Create new script with nonce
            const newScript = document.createElement('script');
            newScript.setAttribute('nonce', nonce);
            newScript.textContent = code;
            
            // Replace old script
            script.parentNode.replaceChild(newScript, script);
            
            converted.push({
                script: newScript,
                nonce,
                code: code.substring(0, 50) + '...'
            });
        });
        
        return converted;
    }
    
    // Hash-based CSP for static content
    generateHashCSP(content, algorithm = 'sha256') {
        const encoder = new TextEncoder();
        const data = encoder.encode(content);
        
        return crypto.subtle.digest(algorithm.toUpperCase(), data)
            .then(buffer => {
                const hashArray = Array.from(new Uint8Array(buffer));
                const hashBase64 = btoa(String.fromCharCode(...hashArray));
                return `'${algorithm}-${hashBase64}'`;
            });
    }
    
    // Generate CSP for all inline scripts using hashes
    async generateHashBasedCSP() {
        const scripts = document.querySelectorAll('script:not([src])');
        const styles = document.querySelectorAll('style');
        const hashes = {
            'script-src': ["'self'"],
            'style-src': ["'self'"]
        };
        
        // Generate hashes for scripts
        for (const script of scripts) {
            const hash = await this.generateHashCSP(script.textContent);
            hashes['script-src'].push(hash);
        }
        
        // Generate hashes for styles
        for (const style of styles) {
            const hash = await this.generateHashCSP(style.textContent);
            hashes['style-src'].push(hash);
        }
        
        return hashes;
    }
}

// Usage example
const nonceManager = new CSPNonceManager();

// Create a nonced script
const { script, nonce } = nonceManager.createNoncedScript('console.log("Secure inline script!");');
document.body.appendChild(script);

CSP Testing Framework

Test CSP implementations:

// CSP Testing Framework
class CSPTester {
    constructor() {
        this.tests = [];
        this.results = {
            passed: 0,
            failed: 0,
            violations: []
        };
    }
    
    // Add test case
    addTest(name, testFn, expectedResult) {
        this.tests.push({
            name,
            testFn,
            expectedResult,
            result: null
        });
    }
    
    // Run all tests
    async runTests() {
        console.log('Running CSP tests...');
        
        for (const test of this.tests) {
            try {
                test.result = await this.runTest(test);
                
                if (test.result.success) {
                    this.results.passed++;
                    console.log(`✓ ${test.name}`);
                } else {
                    this.results.failed++;
                    console.error(`✗ ${test.name}: ${test.result.error}`);
                }
            } catch (error) {
                test.result = { success: false, error: error.message };
                this.results.failed++;
                console.error(`✗ ${test.name}: ${error.message}`);
            }
        }
        
        return this.results;
    }
    
    // Run individual test
    async runTest(test) {
        return new Promise((resolve) => {
            let violationOccurred = false;
            
            const violationHandler = (e) => {
                violationOccurred = true;
                this.results.violations.push({
                    test: test.name,
                    violation: {
                        directive: e.violatedDirective,
                        blocked: e.blockedURI
                    }
                });
            };
            
            document.addEventListener('securitypolicyviolation', violationHandler);
            
            // Run test
            try {
                test.testFn();
                
                // Wait for potential violation
                setTimeout(() => {
                    document.removeEventListener('securitypolicyviolation', violationHandler);
                    
                    const success = test.expectedResult === 'blocked' ? 
                                  violationOccurred : 
                                  !violationOccurred;
                    
                    resolve({
                        success,
                        error: success ? null : 'Unexpected CSP behavior'
                    });
                }, 100);
            } catch (error) {
                document.removeEventListener('securitypolicyviolation', violationHandler);
                resolve({
                    success: test.expectedResult === 'error',
                    error: error.message
                });
            }
        });
    }
    
    // Common test suite
    setupCommonTests() {
        // Test inline script blocking
        this.addTest('Inline script should be blocked', () => {
            const script = document.createElement('script');
            script.textContent = 'window.cspTestInline = true;';
            document.body.appendChild(script);
        }, 'blocked');
        
        // Test eval blocking
        this.addTest('eval() should be blocked', () => {
            eval('window.cspTestEval = true;');
        }, 'error');
        
        // Test inline style blocking
        this.addTest('Inline style should be blocked', () => {
            const div = document.createElement('div');
            div.setAttribute('style', 'color: red;');
            document.body.appendChild(div);
        }, 'blocked');
        
        // Test external script from unauthorized domain
        this.addTest('External script from unauthorized domain should be blocked', () => {
            const script = document.createElement('script');
            script.src = 'https://evil.example.com/script.js';
            document.body.appendChild(script);
        }, 'blocked');
        
        // Test data: URI for images
        this.addTest('data: URI for images', () => {
            const img = document.createElement('img');
            img.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs=';
            document.body.appendChild(img);
        }, 'allowed'); // Depends on img-src policy
    }
    
    // Generate test report
    generateReport() {
        const report = {
            summary: {
                total: this.tests.length,
                passed: this.results.passed,
                failed: this.results.failed,
                passRate: ((this.results.passed / this.tests.length) * 100).toFixed(2) + '%'
            },
            tests: this.tests.map(test => ({
                name: test.name,
                expected: test.expectedResult,
                result: test.result
            })),
            violations: this.results.violations
        };
        
        return report;
    }
}

// Usage
const cspTester = new CSPTester();
cspTester.setupCommonTests();
cspTester.runTests().then(results => {
    console.log('CSP Test Report:', cspTester.generateReport());
});

CSP Migration Assistant

Help migrate to stricter CSP:

// CSP Migration Assistant
class CSPMigrationAssistant {
    constructor() {
        this.currentPolicy = null;
        this.targetPolicy = null;
        this.migrationSteps = [];
    }
    
    // Analyze current state
    async analyzeCurrentState() {
        const state = {
            inlineScripts: [],
            inlineStyles: [],
            externalScripts: [],
            externalStyles: [],
            images: [],
            connects: [],
            frames: [],
            workers: []
        };
        
        // Find inline scripts
        document.querySelectorAll('script:not([src])').forEach(script => {
            state.inlineScripts.push({
                content: script.textContent.substring(0, 100),
                location: script.parentElement.tagName,
                hasNonce: script.hasAttribute('nonce')
            });
        });
        
        // Find inline styles
        document.querySelectorAll('style').forEach(style => {
            state.inlineStyles.push({
                content: style.textContent.substring(0, 100),
                hasNonce: style.hasAttribute('nonce')
            });
        });
        
        // Find inline style attributes
        document.querySelectorAll('[style]').forEach(element => {
            state.inlineStyles.push({
                element: element.tagName,
                style: element.getAttribute('style'),
                inline: true
            });
        });
        
        // Find external resources
        const resources = performance.getEntriesByType('resource');
        resources.forEach(resource => {
            const url = new URL(resource.name);
            const type = resource.initiatorType;
            
            switch (type) {
                case 'script':
                    state.externalScripts.push(url.origin);
                    break;
                case 'css':
                case 'link':
                    state.externalStyles.push(url.origin);
                    break;
                case 'img':
                    state.images.push(url.origin);
                    break;
                case 'xmlhttprequest':
                case 'fetch':
                    state.connects.push(url.origin);
                    break;
            }
        });
        
        // Deduplicate
        Object.keys(state).forEach(key => {
            if (Array.isArray(state[key]) && typeof state[key][0] === 'string') {
                state[key] = [...new Set(state[key])];
            }
        });
        
        return state;
    }
    
    // Generate migration plan
    generateMigrationPlan(currentState) {
        const plan = {
            steps: [],
            estimatedEffort: 0,
            riskLevel: 'low'
        };
        
        // Step 1: Deal with inline scripts
        if (currentState.inlineScripts.length > 0) {
            const unnonced = currentState.inlineScripts.filter(s => !s.hasNonce);
            if (unnonced.length > 0) {
                plan.steps.push({
                    priority: 1,
                    task: 'Add nonces to inline scripts',
                    count: unnonced.length,
                    effort: 'medium',
                    implementation: `
// Server-side: Generate nonce for each request
const nonce = crypto.randomBytes(16).toString('base64');

// Add to CSP header
res.setHeader('Content-Security-Policy', 
    \`script-src 'self' 'nonce-\${nonce}';\`
);

// Add to script tags
<script nonce="\${nonce}">
    // Your inline script
</script>`
                });
                plan.estimatedEffort += 3;
            }
        }
        
        // Step 2: Deal with inline styles
        if (currentState.inlineStyles.length > 0) {
            const styleAttrs = currentState.inlineStyles.filter(s => s.inline);
            if (styleAttrs.length > 0) {
                plan.steps.push({
                    priority: 2,
                    task: 'Move inline styles to CSS classes',
                    count: styleAttrs.length,
                    effort: 'high',
                    implementation: `
// Instead of:
<div style="color: red; padding: 10px;">

// Use:
<div class="error-message">

// CSS:
.error-message {
    color: red;
    padding: 10px;
}`
                });
                plan.estimatedEffort += 5;
                plan.riskLevel = 'medium';
            }
        }
        
        // Step 3: Implement strict-dynamic
        plan.steps.push({
            priority: 3,
            task: 'Implement strict-dynamic for scripts',
            effort: 'low',
            implementation: `
// CSP with strict-dynamic
script-src 'nonce-{random}' 'strict-dynamic' https: 'unsafe-inline';

// 'unsafe-inline' is ignored in browsers that support strict-dynamic
// This provides backward compatibility`
        });
        
        // Step 4: Remove unsafe-eval
        plan.steps.push({
            priority: 4,
            task: 'Remove eval() usage',
            effort: 'high',
            implementation: `
// Instead of eval:
eval('console.log("Hello")');

// Use Function constructor or refactor:
new Function('console.log("Hello")')();

// Better: Refactor to avoid dynamic code execution`
        });
        
        // Step 5: Implement report-only mode
        plan.steps.push({
            priority: 0,
            task: 'Deploy in report-only mode first',
            effort: 'low',
            implementation: `
// Use Content-Security-Policy-Report-Only header
Content-Security-Policy-Report-Only: ${this.generateStrictCSP(currentState)}
`
        });
        
        return plan;
    }
    
    // Generate strict CSP based on current state
    generateStrictCSP(state) {
        const policy = {
            'default-src': ["'none'"],
            'script-src': ["'self'", "'nonce-{random}'", "'strict-dynamic'"],
            'style-src': ["'self'", "'nonce-{random}'"],
            'img-src': ["'self'", ...state.images, 'data:'],
            'connect-src': ["'self'", ...state.connects],
            'font-src': ["'self'"],
            'object-src': ["'none'"],
            'media-src': ["'self'"],
            'frame-src': state.frames.length > 0 ? state.frames : ["'none'"],
            'base-uri': ["'self'"],
            'form-action': ["'self'"],
            'frame-ancestors': ["'none'"],
            'upgrade-insecure-requests': [],
            'block-all-mixed-content': []
        };
        
        // Convert to string
        return Object.entries(policy)
            .map(([directive, sources]) => 
                sources.length > 0 ? `${directive} ${sources.join(' ')}` : directive
            )
            .join('; ') + ';';
    }
    
    // Test CSP without applying
    async testCSP(csp) {
        const testResults = {
            wouldBlock: [],
            wouldAllow: [],
            errors: []
        };
        
        // Create a temporary iframe for testing
        const iframe = document.createElement('iframe');
        iframe.style.display = 'none';
        iframe.srcdoc = `
            <!DOCTYPE html>
            <html>
            <head>
                <meta http-equiv="Content-Security-Policy" content="${csp}">
            </head>
            <body>
                <script>
                    window.cspTest = {
                        violations: [],
                        allowed: []
                    };
                    
                    document.addEventListener('securitypolicyviolation', (e) => {
                        window.cspTest.violations.push({
                            directive: e.violatedDirective,
                            blocked: e.blockedURI
                        });
                    });
                </script>
            </body>
            </html>
        `;
        
        document.body.appendChild(iframe);
        
        // Wait for iframe to load
        await new Promise(resolve => setTimeout(resolve, 100));
        
        // Run tests in iframe
        try {
            // Test inline script
            iframe.contentWindow.eval('window.testEval = true;');
        } catch (e) {
            testResults.wouldBlock.push('eval()');
        }
        
        // Clean up
        document.body.removeChild(iframe);
        
        return testResults;
    }
}

// Usage
const migrationAssistant = new CSPMigrationAssistant();
migrationAssistant.analyzeCurrentState().then(state => {
    const plan = migrationAssistant.generateMigrationPlan(state);
    console.log('CSP Migration Plan:', plan);
});

Best Practices for CSP

  1. Start with Report-Only: Test before enforcing
  2. Use Nonces over Unsafe-Inline: More secure and flexible
  3. Implement Strict-Dynamic: Simplifies dynamic script loading
  4. Avoid Unsafe-Eval: Refactor code to remove eval()
  5. Be Specific with Sources: Avoid wildcards and broad permissions
  6. Upgrade Insecure Requests: Force HTTPS for all resources
  7. Use Hash-Based CSP: For static inline content
  8. Monitor Violations: Set up reporting endpoints
  9. Regular Audits: Review and tighten CSP over time
  10. Test Thoroughly: Use automated testing for CSP

Common CSP Mistakes

  • Too Permissive: Using unsafe-inline and unsafe-eval
  • Forgetting object-src: Allows Flash/Java exploits
  • Missing frame-ancestors: Clickjacking vulnerability
  • No Reporting: Flying blind without violation reports
  • Breaking Functionality: Too strict without testing
  • Ignoring Warnings: Browser console CSP warnings
  • Static Nonces: Reusing nonces defeats purpose
  • Mixed Content: HTTP resources on HTTPS pages
  • No Fallback: Not considering older browsers
  • Copy-Paste CSP: Using CSP without understanding

Remember: CSP is a powerful defense-in-depth measure, but it's not a silver bullet. It should be part of a comprehensive security strategy that includes secure coding practices, input validation, and regular security audits.