What is Content Security Policy?
Content Security Policy (CSP) is a powerful security mechanism that defends against Cross-Site Scripting (XSS), clickjacking, and other code injection attacks by declaring which resources browsers may load and execute. CSP policies specify trusted sources for scripts, styles, images, fonts, and other content types through HTTP headers or meta tags. When properly configured, CSP blocks malicious code injection attempts even when vulnerabilities exist in application code, providing defense-in-depth protection beyond input validation and output encoding.
CSP operates through whitelisting rather than blacklisting. Instead of trying to detect and block malicious patterns—an endless game of cat and mouse as attackers devise new exploits—CSP explicitly declares legitimate resource sources and blocks everything else. This positive security model dramatically reduces attack surface. Even if attackers inject script tags into pages, CSP prevents their execution unless the injected sources match whitelist entries, effectively neutralizing XSS attacks.
CSP Directives and Sources
CSP policies consist of directives controlling different resource types. The script-src directive specifies valid sources for JavaScript execution. style-src controls stylesheets and inline CSS. img-src governs image sources. font-src restricts font loading. connect-src limits fetch(), XMLHttpRequest, WebSocket, and EventSource connections. Each directive accepts a list of source expressions defining trusted origins, paths, or special keywords.
Source expressions use various formats allowing flexible policy configuration. The ‘self’ keyword permits resources from the same origin as the document. Specific domains like ‘https://cdn.example.com’ whitelist those origins. Wildcards like ‘https://*.example.com’ allow any subdomain. The ‘unsafe-inline’ keyword permits inline scripts and styles but significantly weakens CSP protection. ‘unsafe-eval’ allows eval() and similar dangerous functions. The ‘none’ keyword blocks all resources for a directive.
// CSP configuration examples
interface CSPDirectives {
'default-src'?: string[];
'script-src'?: string[];
'style-src'?: string[];
'img-src'?: string[];
'font-src'?: string[];
'connect-src'?: string[];
'frame-src'?: string[];
'object-src'?: string[];
'base-uri'?: string[];
'form-action'?: string[];
}
function buildCSPHeader(directives: CSPDirectives): string {
const parts: string[] = [];
for (const [directive, sources] of Object.entries(directives)) {
if (sources && sources.length > 0) {
parts.push(`${directive} ${sources.join(' ')}`);
}
}
return parts.join('; ');
}
// Strict CSP configuration
const strictCSP: CSPDirectives = {
'default-src': ["'none'"],
'script-src': ["'self'", "'nonce-{random}'"],
'style-src': ["'self'", "'nonce-{random}'"],
'img-src': ["'self'", 'https:', 'data:'],
'font-src': ["'self'"],
'connect-src': ["'self'", 'https://api.example.com'],
'frame-src': ["'none'"],
'object-src': ["'none'"],
'base-uri': ["'self'"],
'form-action': ["'self'"]
};
// Production CSP with CDN
const productionCSP: CSPDirectives = {
'default-src': ["'self'"],
'script-src': ["'self'", 'https://cdn.example.com', "'sha256-{hash}'"],
'style-src': ["'self'", 'https://fonts.googleapis.com'],
'img-src': ["'self'", 'https:', 'data:', 'blob:'],
'font-src': ["'self'", 'https://fonts.gstatic.com'],
'connect-src': ["'self'", 'https://api.example.com', 'wss://ws.example.com'],
'frame-src': ["'self'", 'https://player.vimeo.com'],
'object-src': ["'none'"]
};
// Generate header
const cspHeader = buildCSPHeader(productionCSP);
Preventing XSS Attacks with CSP
XSS attacks inject malicious JavaScript into pages exploiting insufficient input validation or output encoding. Traditional defenses attempt to sanitize inputs and escape outputs, but mistakes happen and new attack vectors emerge constantly. CSP provides defense-in-depth by preventing injected scripts from executing even when injection succeeds, dramatically reducing XSS impact.
The most common XSS pattern injects <script> tags with external sources or inline JavaScript. A strict CSP blocking ‘unsafe-inline’ prevents all inline scripts from executing regardless of origin. Attackers cannot execute injected inline event handlers like <img onerror="malicious()"> or <body onload="steal()"> because CSP blocks inline JavaScript entirely. External script injection fails unless attackers control whitelisted domains, which CSP policies carefully restrict.
CSP’s nonce-based approach enables legitimate inline scripts while blocking injected code. Servers generate random nonces (cryptographically random strings) for each page load and include them in both CSP headers and legitimate script tags. Browsers execute only scripts with matching nonces. Since attackers cannot predict nonces for future page loads, they cannot create valid script tags even with injection vulnerabilities. This elegant solution provides strong XSS protection while supporting modern application architectures.
// Implementing nonce-based CSP
class CSPManager {
generateNonce(): string {
const array = new Uint8Array(16);
crypto.getRandomValues(array);
return btoa(String.fromCharCode(...array));
}
buildCSPWithNonce(nonce: string): string {
const directives: CSPDirectives = {
'default-src': ["'self'"],
'script-src': ["'self'", `'nonce-${nonce}'`],
'style-src': ["'self'", `'nonce-${nonce}'`],
'img-src': ["'self'", 'https:', 'data:'],
'font-src': ["'self'"],
'connect-src': ["'self'"],
'object-src': ["'none'"],
'base-uri': ["'self'"]
};
return buildCSPHeader(directives);
}
async handleRequest(request: Request): Promise<Response> {
const nonce = this.generateNonce();
const cspHeader = this.buildCSPWithNonce(nonce);
// Render HTML with nonce in script tags
const html = `
<!DOCTYPE html>
<html>
<head>
<title>Protected Page</title>
<script nonce="${nonce}">
console.log('Legitimate script with nonce');
</script>
</head>
<body>
<h1>Content Security Policy Protected</h1>
<script nonce="${nonce}">
// This script executes because it has the correct nonce
document.addEventListener('DOMContentLoaded', () => {
console.log('Page loaded');
});
</script>
<!-- This would be blocked by CSP -->
<!-- <script>alert('XSS attempt')</script> -->
</body>
</html>
`;
return new Response(html, {
headers: {
'Content-Type': 'text/html',
'Content-Security-Policy': cspHeader
}
});
}
}
// Usage
const cspManager = new CSPManager();
const response = await cspManager.handleRequest(new Request('https://example.com'));
CSP Reporting and Report-Only Mode
CSP violation reporting enables monitoring policy effectiveness and detecting attack attempts. When browsers block resources due to CSP violations, they send reports to configured endpoints documenting what was blocked, from which source, and on which page. This visibility helps security teams identify attack patterns, discover legitimate resources requiring whitelisting, and refine policies based on real-world usage.
Report-Only mode deploys CSP without enforcement allowing testing policies before activation. The Content-Security-Policy-Report-Only header specifies policies that browsers evaluate and report on without actually blocking resources. This testing mode reveals which legitimate resources current policies would block, enabling policy refinement before enforcement prevents real functionality. Gradual deployment through report-only prevents breaking production applications with overly strict policies.
CSP reports contain detailed violation information including the violated directive, blocked URI, document URI, and line numbers for inline violations. Aggregating reports identifies patterns—frequent violations from specific domains indicate needed whitelist entries, violations from unexpected sources suggest attacks or compromised third-party services. Automated report analysis and alerting enable proactive security monitoring and rapid policy updates.
// CSP reporting implementation
interface CSPViolationReport {
'csp-report': {
'document-uri': string;
'violated-directive': string;
'effective-directive': string;
'original-policy': string;
'blocked-uri': string;
'status-code': number;
'source-file'?: string;
'line-number'?: number;
'column-number'?: number;
};
}
class CSPReportHandler {
private violations: Map<string, number> = new Map();
buildReportingCSP(reportUri: string, reportOnly: boolean = false): string {
const directives: CSPDirectives = {
'default-src': ["'self'"],
'script-src': ["'self'", 'https://cdn.example.com'],
'style-src': ["'self'", "'unsafe-inline'"],
'img-src': ["'self'", 'https:', 'data:']
};
const headerName = reportOnly
? 'Content-Security-Policy-Report-Only'
: 'Content-Security-Policy';
const policy = buildCSPHeader(directives);
return `${policy}; report-uri ${reportUri}`;
}
async handleViolationReport(request: Request): Promise<Response> {
try {
const report: CSPViolationReport = await request.json();
const violation = report['csp-report'];
// Log violation
console.warn('CSP Violation:', {
directive: violation['violated-directive'],
blockedUri: violation['blocked-uri'],
documentUri: violation['document-uri'],
sourceFile: violation['source-file'],
lineNumber: violation['line-number']
});
// Track violations for analysis
const key = `${violation['violated-directive']}:${violation['blocked-uri']}`;
this.violations.set(key, (this.violations.get(key) || 0) + 1);
// Alert on suspicious patterns
if (this.violations.get(key)! > 100) {
console.error('High violation count detected:', key);
// Trigger security alert
}
return new Response('Report received', { status: 204 });
} catch (error) {
console.error('Failed to process CSP report:', error);
return new Response('Error processing report', { status: 400 });
}
}
getViolationStats() {
const stats = Array.from(this.violations.entries())
.map(([key, count]) => ({ violation: key, count }))
.sort((a, b) => b.count - a.count);
return stats;
}
}
// Usage
const reportHandler = new CSPReportHandler();
// Configure CSP with reporting
const cspWithReporting = reportHandler.buildReportingCSP(
'https://example.com/csp-report',
false // Set true for report-only mode
);
Using CSP with CorsProxy
CSP and CORS policies work together but serve different purposes. CORS controls which origins can make requests from browsers to servers. CSP controls which resources browsers will load and execute within pages. When using CorsProxy, CSP policies remain active enforcing resource loading restrictions even though CORS policies are handled by the proxy.
To use CorsProxy while maintaining CSP protection, you must add https://corsproxy.io to your connect-src directive:
// Fix CSP blocking of CorsProxy requests
// Add this to your CSP header:
const cspHeader = "connect-src 'self' https://corsproxy.io; default-src 'self'";
// Then use CorsProxy normally:
const proxyUrl = `https://corsproxy.io/?url=${encodeURIComponent('https://api.example.com/data')}`;
const response = await fetch(proxyUrl); // Now allowed by CSP!
const data = await response.json();
// With full configuration:
const authenticatedUrl = `https://corsproxy.io/?url=${encodeURIComponent('https://api.example.com/data')}&key=your-api-key&type=residential`;
Applications using CorsProxy must include CorsProxy domains in connect-src directives for fetch() and XMLHttpRequest to work. Since CorsProxy acts as an intermediary, browsers make connections to corsproxy.io rather than final destinations. CSP policies must whitelist this intermediate hop while maintaining strict policies for other resource types preventing XSS and injection attacks.
Properly configuring CSP with proxy services requires understanding request flows. The connect-src directive permits HTTPS connections to CorsProxy. Response data from proxied requests still undergoes CSP evaluation—if proxied responses include scripts or styles from non-whitelisted sources, CSP blocks them. This layered security ensures CorsProxy usage doesn’t bypass CSP protection while enabling necessary cross-origin data access.
// CSP configuration for applications using CorsProxy
class CorsProxyCSP {
buildCSPForProxy(): string {
const directives: CSPDirectives = {
'default-src': ["'self'"],
'script-src': ["'self'", "'nonce-{random}'"],
'style-src': ["'self'", "'nonce-{random}'"],
'img-src': ["'self'", 'https:', 'data:'],
'font-src': ["'self'"],
// Allow connections to CorsProxy
'connect-src': ["'self'", 'https://corsproxy.io'],
'object-src': ["'none'"],
'base-uri': ["'self'"]
};
return buildCSPHeader(directives);
}
async fetchThroughProxy(url: string, apiKey: string): Promise<any> {
const proxyUrl = `https://corsproxy.io/?url=${encodeURIComponent(url)}&key=${apiKey}&type=residential&colo=fra`;
// This fetch is allowed by connect-src: https://corsproxy.io
const response = await fetch(proxyUrl, {
headers: {
'Accept': 'application/json'
}
});
return response.json();
}
}
// Usage
const corsProxyCSP = new CorsProxyCSP();
const cspHeader = corsProxyCSP.buildCSPForProxy();
// Set CSP header in responses
const html = `<!DOCTYPE html>
<html>
<head>
<script nonce="{nonce}">
// Fetch through CorsProxy - allowed by connect-src
fetch('https://corsproxy.io/?url=https://api.example.com/data&key=apikey')
.then(r => r.json())
.then(data => console.log(data));
</script>
</head>
</html>`;
const response = new Response(html, {
headers: {
'Content-Type': 'text/html',
'Content-Security-Policy': cspHeader
}
});
Common CSP Pitfalls and Solutions
The ‘unsafe-inline’ directive defeats most CSP protection by allowing any inline script or style execution. While it permits existing inline code without modification, it also enables all XSS attacks exploiting inline injection. Avoid ‘unsafe-inline’ entirely, refactoring inline scripts to external files or using nonce/hash whitelisting instead. The convenience rarely justifies the massive security downgrade.
Overly permissive wildcard sources undermine CSP effectiveness. Policies like script-src: ‘https:’ or script-src: ’*’ allow scripts from any HTTPS origin or any origin respectively, providing minimal protection against XSS. Attackers can host malicious scripts on compromised or malicious HTTPS sites and inject references passing overly permissive policies. Use specific domain whitelisting combined with ‘self’ rather than broad wildcards.
Missing directives inherit from default-src creating unintended permissions. If default-src: ‘self’ exists without explicit script-src, scripts from ‘self’ are permitted. Adding a CDN to img-src doesn’t automatically extend to script-src—each directive requires explicit configuration. Always specify critical directives like script-src, style-src, and connect-src separately even when they match default-src to avoid unexpected inheritance behavior.
CSP Best Practices
Start with strict policies and selectively relax as needed rather than beginning permissively and attempting to restrict later. A policy starting with default-src: ‘none’ and script-src: ‘self’ provides maximum protection, requiring explicit whitelisting for every resource type. This approach surfaces all required sources during testing, enabling informed decisions about each addition. Gradual relaxation based on requirements beats gradual restriction from permissive defaults.
Deploy CSP in report-only mode before enforcement capturing violations without breaking functionality. Monitor reports for several weeks identifying legitimate resources requiring whitelisting and potential attack attempts. Analyze patterns—frequent violations from specific sources indicate needed policy updates, while sporadic violations from random domains suggest attack probes. Only enforce policies after report analysis confirms they won’t break legitimate functionality.
Maintain separate policies for development, staging, and production environments. Development needs permissive policies allowing localhost and development tools. Staging should mirror production with reporting enabled for final validation. Production enforces strict policies with minimal exceptions. Environment-specific configuration enables appropriate security without hindering development workflows or requiring developers to disable security features locally.