CSP: Content Security Policy Deep Dive
Content Security Policy is arguably the most important HTTP security header for modern web applications. It provides a declarative way to tell the browser exactly which resources are allowed to load and execute on your page. A well-configured CSP is the strongest defense against cross-site scripting (XSS), data injection attacks, and many forms of content hijacking. However, CSP is also one of the most commonly misconfigured headers — a single mistake can either break your site or leave it unprotected. This lesson provides a thorough walkthrough of every CSP concept you need to master.
How CSP works
When a browser receives a Content-Security-Policy header (or a <meta> tag equivalent), it parses the policy into a set of directives. Each directive specifies which sources are allowed for a particular type of resource. When the browser encounters a resource — a script tag, an image, a stylesheet, a font, an iframe — it checks the CSP policy to see if that resource's origin is permitted. If it is not, the browser blocks the resource and logs a violation to the console.
CSP directives
Each directive controls a specific type of resource. Here are the most important ones:
default-src
The fallback directive. If a more specific directive is not defined, the browser uses default-src. Setting default-src 'none' and then explicitly allowing each resource type is the most secure approach.
default-src 'none';
script-src
Controls which JavaScript sources can execute. This is the most critical directive for preventing XSS. Common values include 'self' (same-origin scripts), specific domains, nonces, and hashes.
script-src 'self' 'nonce-abc123' https://cdn.example.com;
style-src
Controls which CSS sources can be applied. Similar to script-src, you can use 'self', specific domains, nonces, and hashes.
style-src 'self' 'nonce-xyz789' https://fonts.googleapis.com;
img-src
Controls which image sources are allowed. The data: scheme is often needed for inline images (base64-encoded). The blob: scheme may be needed for dynamically generated images.
img-src 'self' data: https:;
connect-src
Controls which URLs the page can connect to via fetch(), XMLHttpRequest, WebSocket, EventSource, and the Beacon API. This is crucial for controlling where your application sends data.
connect-src 'self' https://api.example.com wss://ws.example.com;
font-src
Controls which sources can serve fonts. If you use Google Fonts or a self-hosted font CDN, you need to allow those origins here.
font-src 'self' https://fonts.gstatic.com;
frame-src
Controls which sources can be loaded in <iframe>, <frame>, <object>, and <embed> elements. If your page does not embed external content, set this to 'none'.
frame-src 'self' https://www.youtube.com;
frame-ancestors
Controls which pages are allowed to embed your page in a frame. This is the CSP replacement for X-Frame-Options. Setting it to 'none' prevents clickjacking entirely.
frame-ancestors 'none';
object-src
Controls sources for <object>, <embed>, and <applet> elements. These elements are rarely needed in modern applications and should almost always be set to 'none'.
object-src 'none';
base-uri
Restricts the URLs that can be used in a <base> element. An attacker who can inject a <base> tag can redirect all relative URLs on your page to a malicious server. Always set this to 'self' or 'none'.
base-uri 'self';
Nonces vs hashes vs strict-dynamic
One of the most important decisions when configuring CSP is how to allow your own inline scripts and styles. There are three main approaches, each with trade-offs.
Nonces
A nonce (number used once) is a random value generated by the server on every page load. The server adds the nonce to both the CSP header and the nonce attribute of each allowed script or style tag. The browser only executes scripts and styles whose nonce attribute matches the one in the CSP header.
# Server generates a random nonce
$nonce = base64_encode(random_bytes(16));
# CSP header includes the nonce
Content-Security-Policy: script-src 'nonce-YWJjMTIz' 'strict-dynamic';
# HTML script tags include the matching nonce
<script nonce="YWJjMTIz">console.log('This runs');</script>
Advantages of nonces
- Works with dynamically generated inline scripts.
- No need to compute or maintain hash values.
- Simple to implement in server-side rendered applications.
Requirements
- Must be cryptographically random (at least 128 bits of entropy).
- Must be unique for every page load — reusing nonces defeats the purpose.
- Requires server-side rendering to inject the nonce into both the header and HTML.
Hashes
Instead of a nonce, you can specify the exact SHA-256 (or SHA-384, SHA-512) hash of allowed inline scripts. The browser computes the hash of each inline script it encounters and only executes those that match a hash in the CSP header.
Content-Security-Policy: script-src 'sha256-RFWPLDbv2BY+rCkDzsE+0fr8ylGr2R2faWMhq4lfEQc=';
Advantages of hashes
- Works with static sites and CDNs where server-side rendering is not available.
- Does not require generating and injecting a nonce on every request.
Disadvantages
- Any change to the script content (even whitespace) changes the hash, requiring a CSP update.
- Difficult to manage when scripts change frequently.
- Does not work for dynamically generated scripts.
strict-dynamic
The 'strict-dynamic' keyword tells the browser: "Trust any script that is loaded by a script I already trust." When you combine 'strict-dynamic' with a nonce, you only need to add the nonce to your top-level script tags. Any scripts those scripts load dynamically (e.g., via document.createElement('script')) are automatically trusted.
Content-Security-Policy: script-src 'nonce-YWJjMTIz' 'strict-dynamic';
This is the recommended approach for modern applications because it is both secure and practical. It eliminates the need to maintain an allowlist of CDN domains, which can be bypassed via script gadgets.
Report-uri and report-to for monitoring violations
CSP provides two mechanisms for reporting policy violations, allowing you to monitor for attacks and identify misconfigurations.
report-uri (deprecated but still widely used)
Content-Security-Policy: default-src 'self'; report-uri /csp-violations;
When a violation occurs, the browser sends a JSON POST request to the specified URI containing details about the blocked resource, the violated directive, and the page where it occurred.
report-to (modern replacement)
Report-To: {"group":"csp","max_age":86400,"endpoints":[{"url":"https://example.com/csp-reports"}]}
Content-Security-Policy: default-src 'self'; report-to csp;
The report-to directive uses the Reporting API, which batches and deduplicates reports for better performance.
Report-Only mode
Before enforcing a CSP, use the Content-Security-Policy-Report-Only header to run the policy in monitoring mode. Violations are reported but not blocked. This lets you identify everything that would break before you enforce the policy.
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self'; report-uri /csp-violations;
Common pitfalls
unsafe-inline
Adding 'unsafe-inline' to your CSP is the most common mistake. It tells the browser to allow all inline scripts and styles, which completely negates CSP's protection against XSS. If an attacker can inject <script>malicious code</script> into your page, 'unsafe-inline' lets it run.
# BAD - defeats the purpose of CSP
script-src 'self' 'unsafe-inline';
# GOOD - use nonces instead
script-src 'self' 'nonce-abc123';
unsafe-eval
'unsafe-eval' allows the use of eval(), new Function(), setTimeout('string'), and other string-to-code APIs. These are common vectors for XSS attacks. Some libraries require eval (e.g., older template engines), but modern alternatives exist for nearly all use cases.
Overly broad allowlists
Allowing entire CDN domains like https://cdn.jsdelivr.net or https://cdnjs.cloudflare.com in script-src is dangerous. Attackers can host malicious scripts on these same CDNs and bypass your CSP. Use nonces or hashes with 'strict-dynamic' instead of domain allowlists for scripts.
Forgetting about inline event handlers
Inline event handlers like onclick="doSomething()" are blocked by CSP even with nonces. You cannot add a nonce to an inline event handler. The solution is to use addEventListener() in a nonced script block instead.
# BAD - blocked by CSP
<button onclick="doSomething()">Click</button>
# GOOD - use addEventListener in a nonced script
<button id="myBtn">Click</button>
<script nonce="abc123">
document.getElementById('myBtn').addEventListener('click', doSomething);
</script>
Step-by-step guide to implementing CSP
- Audit your resources: Open your browser's developer tools (Network tab) and catalog every resource your page loads — scripts, styles, images, fonts, API calls, iframes, and more. Note their origins.
- Start with report-only: Deploy a strict CSP in report-only mode. Set
default-src 'none'and add directives for each resource type based on your audit. - Monitor violations: Collect and analyze violation reports for a few days. Each report tells you what was blocked and why. Adjust your policy to allow legitimate resources.
- Replace inline scripts with nonces: Add nonce generation to your server. Assign nonces to all inline
<script>and<style>tags. Remove allonclick,onload, and other inline event handlers. - Add strict-dynamic: Include
'strict-dynamic'inscript-srcso that scripts loaded by your nonced scripts are automatically trusted. - Test thoroughly: Verify all functionality works with the CSP enforced. Test in multiple browsers. Check for console errors related to CSP violations.
- Enforce the policy: Switch from
Content-Security-Policy-Report-OnlytoContent-Security-Policy. Keepreport-toactive so you can continue monitoring. - Iterate: CSP is not set-and-forget. When you add new features, third-party integrations, or analytics tools, update your CSP accordingly.
How the CodeFrog website uses CSP with nonces
The very page you are reading right now uses CSP with nonces. Here is how it works:
- When your browser requests this page, the PHP server generates a unique cryptographic nonce using
generate_script_nonce(). - The nonce is included in the
Content-Security-Policyheader sent with the response:script-src 'nonce-...' 'strict-dynamic'. - Every
<script>tag on the page includes the matchingnonceattribute:<script nonce="...">. - When your browser parses the page, it checks each script's nonce against the CSP header. Only scripts with the correct nonce execute.
- If an attacker managed to inject a script tag, it would not have the correct nonce and would be blocked by the browser.
This is a practical, real-world implementation of CSP with nonces. Every page on codefrog.app generates a fresh nonce on every request, ensuring that even if an attacker could predict the nonce from one request, it would not be valid for the next.
'unsafe-inline') are flagged as security findings.
Resources
- MDN: Content Security Policy — Comprehensive reference from Mozilla
- Google CSP Evaluator — Paste your CSP and get feedback on its strength
- web.dev: Strict CSP — Google's guide to implementing a strict CSP
- Report URI — Free and paid service for collecting and analyzing CSP violation reports