← devnestio

CSP Evaluator

Security Grader

Content-Security-Policy Header Value

Load sample:
🔒

Paste a CSP header value above and click Evaluate CSP to get an instant security grade and detailed analysis.

' }); } } // ---- script-src ---- const scriptSrc = effectiveSrc(directives, 'script-src'); if (!directives['script-src']) { if (directives['default-src']) { issues.push({ id: 'no-script-src', severity: 'high', title: 'No explicit script-src (falling back to default-src)', description: 'Best practice is to define an explicit script-src. Relying on default-src may be overly broad.', bypass: null }); } } if (scriptSrc) { if (hasWildcard(scriptSrc)) { issues.push({ id: 'script-src-wildcard', severity: 'critical', title: 'script-src allows * (wildcard)', description: 'Wildcard in script-src allows loading scripts from any origin, completely defeating XSS protection.', bypass: '' }); } if (hasValue(scriptSrc, "'unsafe-inline'")) { const hasNonce = scriptSrc.some(v => v.startsWith("'nonce-")); const hasHash = scriptSrc.some(v => v.startsWith("'sha256-") || v.startsWith("'sha384-") || v.startsWith("'sha512-")); if (!hasNonce && !hasHash) { issues.push({ id: 'script-src-unsafe-inline', severity: 'critical', title: "script-src contains 'unsafe-inline'", description: "'unsafe-inline' allows execution of any inline script, enabling XSS via injected ' }); } } if (hasValue(scriptSrc, "'unsafe-eval'")) { issues.push({ id: 'script-src-unsafe-eval', severity: 'high', title: "script-src contains 'unsafe-eval'", description: "'unsafe-eval' permits use of eval(), setTimeout(string), setInterval(string), and new Function(string). Attackers who can inject data into these calls can execute arbitrary scripts.", bypass: 'eval(atob("YWxlcnQoMSk="))' }); } if (hasScheme(scriptSrc, 'data:')) { issues.push({ id: 'script-src-data', severity: 'high', title: "script-src allows 'data:' URIs", description: "The data: scheme in script-src allows execution of base64-encoded scripts, which can be used to bypass other restrictions.", bypass: '' }); } if (hasScheme(scriptSrc, 'http:')) { issues.push({ id: 'script-src-http', severity: 'high', title: "script-src allows http: scheme", description: "Allowing http: in script-src permits loading scripts over insecure connections, enabling man-in-the-middle attacks that inject malicious scripts.", bypass: null }); } // Check for many domains (overly broad allowlist) const urlSources = scriptSrc.filter(v => v.startsWith('http://') || v.startsWith('https://') || v.startsWith('//')); if (urlSources.length >= 5) { issues.push({ id: 'script-src-many-origins', severity: 'medium', title: `script-src has ${urlSources.length} allowed origins`, description: "A large allowlist increases attack surface. Every allowed CDN or third-party domain is a potential XSS pivot if any of their served files can be influenced by an attacker. Consider using nonces or hashes instead.", bypass: null }); } // Check nonce length hint (we can only inspect the declared nonce, not actual entropy) const nonces = scriptSrc.filter(v => v.startsWith("'nonce-")); for (const nonce of nonces) { const b64 = nonce.slice(7, -1); // base64: 1 char ≈ 6 bits; 128 bits = ceil(128/6) = 22 chars if (b64.length < 22) { issues.push({ id: 'short-nonce', severity: 'medium', title: 'Nonce appears to be too short (<128 bits)', description: `Nonce "${b64}" encodes fewer than 128 bits of entropy. NIST recommends at least 128 bits to prevent guessing attacks.`, bypass: null }); } } } // ---- object-src ---- const objectSrc = effectiveSrc(directives, 'object-src'); if (!directives['object-src']) { const defHasNone = directives['default-src'] && hasValue(directives['default-src'], "'none'"); if (!defHasNone) { issues.push({ id: 'no-object-src-none', severity: 'high', title: "Missing object-src 'none'", description: "Without object-src 'none', plugins like Flash (legacy) or PDF viewers can be embedded. These can be used to execute scripts and bypass CSP in older browsers.", bypass: null }); } } else if (!hasValue(directives['object-src'], "'none'")) { issues.push({ id: 'object-src-not-none', severity: 'high', title: "object-src is not set to 'none'", description: "object-src should be set to 'none' to block plugin-based attacks (Flash, ActiveX, PDF readers with script execution).", bypass: null }); } // ---- base-uri ---- if (!directives['base-uri']) { issues.push({ id: 'no-base-uri', severity: 'medium', title: 'Missing base-uri directive', description: "Without base-uri restriction, attackers who can inject a tag can redirect all relative URLs to an attacker-controlled origin. Recommend: base-uri 'self' or base-uri 'none'.", bypass: '' }); } // ---- frame-ancestors ---- if (!directives['frame-ancestors']) { issues.push({ id: 'no-frame-ancestors', severity: 'medium', title: 'Missing frame-ancestors directive', description: "Without frame-ancestors, your page can be embedded in any iframe, enabling clickjacking attacks. This directive supersedes X-Frame-Options. Recommend: frame-ancestors 'none' or 'self'.", bypass: null }); } else if (hasWildcard(directives['frame-ancestors'])) { issues.push({ id: 'frame-ancestors-wildcard', severity: 'medium', title: "frame-ancestors allows * (any origin can embed)", description: "A wildcard in frame-ancestors defeats clickjacking protection, allowing any site to embed your page in an iframe.", bypass: null }); } // ---- form-action ---- if (!directives['form-action']) { issues.push({ id: 'no-form-action', severity: 'low', title: 'Missing form-action directive', description: "Without form-action, forms can submit to any URL. If an attacker can inject a
tag, they can exfiltrate data. Recommend: form-action 'self'.", bypass: null }); } // ---- upgrade-insecure-requests / block-all-mixed-content ---- if (!directives['upgrade-insecure-requests'] && !directives['block-all-mixed-content']) { issues.push({ id: 'no-upgrade-insecure', severity: 'low', title: 'Missing upgrade-insecure-requests or block-all-mixed-content', description: "Without these directives, mixed content (HTTP resources on HTTPS pages) may load. Add upgrade-insecure-requests to auto-upgrade HTTP to HTTPS.", bypass: null }); } // ---- http: in non-script directives ---- const allDirs = Object.keys(directives); for (const dir of allDirs) { if (dir === 'script-src') continue; // already checked above with higher severity if (hasScheme(directives[dir], 'http:') && dir !== 'report-uri') { issues.push({ id: `http-scheme-${dir}`, severity: 'medium', title: `http: scheme allowed in ${dir}`, description: `Allowing http: in ${dir} allows resources to be loaded over insecure connections, enabling MITM attacks.`, bypass: null }); } } // ---- report-uri deprecated ---- if (directives['report-uri'] && !directives['report-to']) { issues.push({ id: 'report-uri-deprecated', severity: 'info', title: 'report-uri is deprecated — consider report-to', description: "The report-uri directive is deprecated in CSP Level 3. Modern browsers prefer the Reporting API's report-to directive. You can include both for compatibility.", bypass: null }); } // ---- img-src wildcard ---- if (directives['img-src'] && hasWildcard(directives['img-src'])) { issues.push({ id: 'img-src-wildcard', severity: 'low', title: 'img-src allows * (any image source)', description: "While less dangerous than script-src wildcards, allowing all image sources enables pixel-tracking, CSRF via image URLs, and information leakage in some scenarios.", bypass: null }); } // ---- style-src unsafe-inline ---- const styleSrc = effectiveSrc(directives, 'style-src'); if (styleSrc && hasValue(styleSrc, "'unsafe-inline'")) { const hasNonce = styleSrc.some(v => v.startsWith("'nonce-")); const hasHash = styleSrc.some(v => v.startsWith("'sha256-") || v.startsWith("'sha384-") || v.startsWith("'sha512-")); if (!hasNonce && !hasHash) { issues.push({ id: 'style-src-unsafe-inline', severity: 'medium', title: "style-src contains 'unsafe-inline'", description: "'unsafe-inline' in style-src allows injected inline styles, which can be used for UI redressing attacks (fake login forms, clickjacking via CSS).", bypass: '
Fake login
' }); } } return issues; } /* ============================================================ GRADE CALCULATOR ============================================================ */ function calculateGrade(issues) { let penalty = 0; const counts = { critical: 0, high: 0, medium: 0, low: 0, info: 0 }; for (const issue of issues) { penalty += SEVERITY_WEIGHT[issue.severity] || 0; counts[issue.severity]++; } let grade, color, desc; if (counts.critical === 0 && counts.high === 0 && counts.medium === 0 && penalty <= 5) { grade = 'A+'; color = '#4ade80'; desc = 'Excellent — very strong policy'; } else if (counts.critical === 0 && counts.high === 0 && penalty <= 30) { grade = 'A'; color = '#4ade80'; desc = 'Strong policy with minor improvements possible'; } else if (counts.critical === 0 && counts.high <= 1 && penalty <= 60) { grade = 'B'; color = '#86efac'; desc = 'Good policy with some gaps'; } else if (counts.critical === 0 && penalty <= 120) { grade = 'C'; color = '#fbbf24'; desc = 'Moderate policy — several issues need attention'; } else if (counts.critical <= 1 && penalty <= 200) { grade = 'D'; color = '#fb923c'; desc = 'Weak policy — critical issues present'; } else { grade = 'F'; color = '#f87171'; desc = 'Failing — policy provides little to no protection'; } return { grade, color, desc, counts, penalty }; } /* ============================================================ HARDENED CSP GENERATOR ============================================================ */ function generateHardenedCSP(directives, issues) { const issueIds = new Set(issues.map(i => i.id)); const hardened = {}; // Copy existing directives, cleaning up issues for (const [dir, vals] of Object.entries(directives)) { let newVals = [...vals]; if (dir === 'script-src' || dir === 'default-src') { newVals = newVals.filter(v => v !== '*' && v.toLowerCase() !== 'http:'); if (hasValue(newVals, "'unsafe-inline'")) { const hasNonceOrHash = newVals.some(v => v.startsWith("'nonce-") || v.startsWith("'sha256-") || v.startsWith("'sha384-") || v.startsWith("'sha512-")); if (!hasNonceOrHash) { // Replace with a nonce placeholder newVals = newVals.filter(v => v.toLowerCase() !== "'unsafe-inline'"); newVals.push("'nonce-{REPLACE_WITH_RANDOM}'"); } } if (hasValue(newVals, "'unsafe-eval'")) { newVals = newVals.filter(v => v.toLowerCase() !== "'unsafe-eval'"); // Keep commented note } } if (dir === 'img-src') { // Wildcard on img-src is low severity, keep with note } hardened[dir] = newVals; } // Ensure key directives exist if (!hardened['object-src']) hardened['object-src'] = ["'none'"]; else if (!hasValue(hardened['object-src'], "'none'")) hardened['object-src'] = ["'none'"]; if (!hardened['base-uri']) hardened['base-uri'] = ["'self'"]; if (!hardened['frame-ancestors']) hardened['frame-ancestors'] = ["'none'"]; if (!hardened['form-action']) hardened['form-action'] = ["'self'"]; if (!hardened['upgrade-insecure-requests'] && !hardened['block-all-mixed-content']) { hardened['upgrade-insecure-requests'] = []; } // Remove report-uri if report-to is missing (suggest adding report-to) // Build the CSP string const parts = []; const order = [ 'default-src', 'script-src', 'style-src', 'img-src', 'font-src', 'connect-src', 'media-src', 'object-src', 'frame-src', 'frame-ancestors', 'base-uri', 'form-action', 'upgrade-insecure-requests', 'block-all-mixed-content', 'worker-src', 'manifest-src', 'prefetch-src', 'report-to', 'report-uri' ]; const ordered = [...order.filter(k => hardened[k] !== undefined)]; const rest = Object.keys(hardened).filter(k => !order.includes(k)); for (const dir of [...ordered, ...rest]) { const vals = hardened[dir]; if (vals.length === 0) { parts.push(dir); } else { parts.push(`${dir} ${vals.join(' ')}`); } } return parts.join(';\n') + ';'; } /* ============================================================ DIRECTIVE STATUS CALCULATOR ============================================================ */ function getDirectiveStatus(name, vals, issues) { const relatedIssues = issues.filter(i => { const lower = i.id.toLowerCase(); return lower.includes(name.replace('-', '')) || lower.includes(name.split('-')[0]) || (i.title && i.title.toLowerCase().includes(name)); }); if (relatedIssues.some(i => i.severity === 'critical')) return { status: 'critical', dot: 'dot-critical', label: 'Critical' }; if (relatedIssues.some(i => i.severity === 'high')) return { status: 'warning', dot: 'dot-warning', label: 'Warning' }; if (relatedIssues.some(i => i.severity === 'medium')) return { status: 'warning', dot: 'dot-warning', label: 'Caution' }; return { status: 'secure', dot: 'dot-secure', label: 'OK' }; } function formatValues(vals) { return vals.map(v => { if (v === '*') return `${v}`; if (v.startsWith("'nonce-")) return `${v}`; if (v.startsWith("'sha256-") || v.startsWith("'sha384-") || v.startsWith("'sha512-")) return `${v}`; if (v.startsWith("'")) return `${v}`; if (v.startsWith('http://') || v.startsWith('https://') || v.startsWith('//')) return `${v}`; return `${v}`; }).join(' '); } /* ============================================================ MISSING DIRECTIVES PANEL ============================================================ */ const IMPORTANT_DIRECTIVES = [ 'default-src', 'script-src', 'style-src', 'img-src', 'font-src', 'connect-src', 'object-src', 'frame-ancestors', 'base-uri', 'form-action' ]; /* ============================================================ SAMPLES ============================================================ */ const SAMPLES = { weak: "default-src *; script-src * 'unsafe-inline' 'unsafe-eval'; style-src * 'unsafe-inline'; img-src *", medium: "default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com https://ajax.googleapis.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: https:; font-src 'self' https://fonts.gstatic.com; connect-src 'self'; object-src 'none'; report-uri /csp-report", strong: "default-src 'none'; script-src 'self' 'nonce-rAnd0mN0nce128bit==' https://cdn.example.com; style-src 'self' 'nonce-rAnd0mN0nce128bit=='; img-src 'self' data:; font-src 'self'; connect-src 'self' https://api.example.com; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'self'; upgrade-insecure-requests; report-to csp-endpoint" }; function loadSample(type) { document.getElementById('cspInput').value = SAMPLES[type]; evaluateCSP(); } function clearInput() { document.getElementById('cspInput').value = ''; document.getElementById('results').classList.add('hidden'); document.getElementById('placeholder').classList.remove('hidden'); } /* ============================================================ MAIN EVALUATOR ============================================================ */ function evaluateCSP() { const raw = document.getElementById('cspInput').value.trim(); if (!raw) { clearInput(); return; } const directives = parseCSP(raw); const issues = runChecks(directives); const { grade, color, desc, counts, penalty } = calculateGrade(issues); const hardened = generateHardenedCSP(directives, issues); // Show results, hide placeholder document.getElementById('placeholder').classList.add('hidden'); document.getElementById('results').classList.remove('hidden'); // Grade const gradeLetter = document.getElementById('gradeLetter'); gradeLetter.textContent = grade; gradeLetter.style.color = color; document.getElementById('gradeScore').textContent = `Penalty score: ${penalty}`; document.getElementById('gradeDesc').textContent = desc; // Counts document.getElementById('countCritical').textContent = counts.critical; document.getElementById('countHigh').textContent = counts.high; document.getElementById('countMedium').textContent = counts.medium; document.getElementById('countLow').textContent = counts.low; document.getElementById('countInfo').textContent = counts.info; // Issues list const issuesList = document.getElementById('issuesList'); issuesList.innerHTML = ''; if (issues.length === 0) { issuesList.innerHTML = '
No issues found — strong CSP!
'; } else { const sorted = [...issues].sort((a, b) => (SEVERITY_WEIGHT[b.severity] || 0) - (SEVERITY_WEIGHT[a.severity] || 0)); for (const issue of sorted) { const div = document.createElement('div'); div.className = `issue-item ${issue.severity}`; let bypassHtml = ''; if (issue.bypass) { bypassHtml = `
Example Bypass Payload
${escHtml(issue.bypass)}
`; } div.innerHTML = `
${issue.severity} ${escHtml(issue.title)}
${escHtml(issue.description)}
${bypassHtml} `; issuesList.appendChild(div); } } // Directive table const tbody = document.getElementById('directiveTableBody'); tbody.innerHTML = ''; for (const [name, vals] of Object.entries(directives)) { const { dot, label } = getDirectiveStatus(name, vals, issues); const relIssues = issues.filter(i => i.id.toLowerCase().includes(name.replace('-src','').replace('-','')) || (i.title && i.title.toLowerCase().includes(name))); const notesHtml = relIssues.length > 0 ? relIssues.map(i => `${escHtml(i.title.substring(0, 40))}${i.title.length > 40 ? '…' : ''}`).join('
') : 'OK'; const tr = document.createElement('tr'); tr.innerHTML = ` ${escHtml(name)} ${vals.length ? formatValues(vals) : 'none'} ${label} ${notesHtml} `; tbody.appendChild(tr); } // Missing important directives note const missingDiv = document.getElementById('directivesMissing'); const missing = IMPORTANT_DIRECTIVES.filter(d => !directives[d]); if (missing.length > 0) { missingDiv.innerHTML = '
Missing directives:
' + missing.map(d => `${escHtml(d)} `).join(''); } else { missingDiv.innerHTML = ''; } // Hardened CSP document.getElementById('hardenedCSP').textContent = hardened; } function escHtml(s) { return String(s).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); } function copyHardened(btn) { const text = document.getElementById('hardenedCSP').textContent; navigator.clipboard.writeText(text).then(() => { btn.textContent = 'Copied!'; btn.classList.add('copied'); setTimeout(() => { btn.textContent = 'Copy'; btn.classList.remove('copied'); }, 2000); }); } /* ============================================================ REFERENCE ============================================================ */ const CSP_REFERENCE = [ { name: 'default-src', desc: 'Fallback for all fetch directives not explicitly set. Controls where resources can be loaded from by default.' }, { name: 'script-src', desc: 'Controls sources for JavaScript. The most critical directive for XSS prevention.' }, { name: 'style-src', desc: 'Controls sources for CSS stylesheets. Unsafe-inline enables UI redressing attacks.' }, { name: 'img-src', desc: 'Controls sources for images (including CSS background-image).' }, { name: 'font-src', desc: 'Controls sources for web fonts loaded via @font-face.' }, { name: 'connect-src', desc: 'Controls URLs for XHR, Fetch, WebSockets, EventSource, and Beacon API.' }, { name: 'media-src', desc: 'Controls sources for audio and video elements.' }, { name: 'object-src', desc: 'Controls sources for , , and . Should be set to none.' }, { name: 'frame-src', desc: 'Controls sources for and