A Content Security Policy (CSP) header is a surprisingly flexible and powerful tool for mitigating cross-site scripting (XSS) and other code injection attacks, but most people only see it as a restrictive firewall.
Let’s see it in action. Imagine a simple HTML page hosted on Netlify:
<!DOCTYPE html>
<html>
<head>
<title>CSP Example</title>
<link rel="stylesheet" href="/style.css">
<script src="/script.js"></script>
</head>
<body>
<h1>Hello, CSP!</h1>
<img src="/image.png" alt="Example Image">
<script>
// Inline script
console.log("This is an inline script.");
</script>
<style>
/* Inline style */
body {
background-color: lightblue;
}
</style>
</body>
</html>
Without a CSP header, all these resources (CSS, JS, images) and even inline scripts/styles would load without issue.
Now, let’s add a CSP header via Netlify’s _headers file. Create a file named _headers in the root of your project:
/ _headers
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:;
Here’s what each directive means and how it affects our example page:
default-src 'self': This is the fallback for most other directives. It tells the browser to only allow resources to be loaded from the same origin (your Netlify site’s domain).script-src 'self': This specifically restricts JavaScript. With'self', it meansscript.jswill load, but the inline<script>block will be blocked.style-src 'self': Similar toscript-src, this restricts CSS.style.csswill load, but the inline<style>block will be blocked.img-src 'self' data:: This controls images.image.pngwill load because it’s from'self'. If you wanted to allow images from external CDNs, you’d add their domains here (e.g.,img-src 'self' https://cdn.example.com). Thedata:scheme allows for embedding small images directly as Base64 encoded data URIs, which is useful for icons or small graphics.
When you deploy this with the _headers file, the browser will enforce these rules. The style.css and script.js files will load, and the <img> will display. However, the inline <script> and <style> blocks will be blocked by the browser, and you’ll likely see errors in the browser’s developer console, such as:
Refused to execute inline script because it violates the following Content Security Policy directive: "script-src 'self'". Either the 'unsafe-inline' keyword is allowed or scripts should be loaded from permitted sources.
The core problem CSP solves is preventing attackers from injecting malicious scripts or other content into your web pages. By default, browsers trust anything that looks like it belongs to your site. CSP allows you to explicitly tell the browser: "Only trust these specific sources for these specific types of content."
To allow the inline script, you would need to add the 'unsafe-inline' keyword to script-src:
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self'; img-src 'self' data:;
Caution: While 'unsafe-inline' is convenient, it significantly weakens your CSP by re-enabling the very attacks you’re trying to prevent. A better approach for inline scripts is to use nonces or hashes. For example, to allow a specific inline script, you’d generate a unique, random string (a nonce) on the server for each request, include it in the script-src directive, and add it as an attribute to your script tag:
<script nonce="YOUR_GENERATED_NONCE_HERE">
// This script will now execute
</script>
And your _headers file would look like:
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-YOUR_GENERATED_NONCE_HERE'; style-src 'self'; img-src 'self' data:;
(Note: Netlify’s static headers don’t support dynamic nonces directly; this is more illustrative of the general CSP mechanism. For dynamic nonces on Netlify, you’d typically use a serverless function or a framework integration.)
Similarly, for inline styles, you can use 'unsafe-inline' (again, not recommended for security) or hashes if the style content is predictable.
The true power of CSP lies in its granular control. You can define separate policies for different types of content (script-src, style-src, img-src, font-src, connect-src, media-src, frame-src, etc.) and specify exactly which domains or schemes are allowed for each. For instance, to allow API calls to api.example.com while still restricting everything else to your own origin:
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self'; connect-src 'self' https://api.example.com; img-src 'self' data:;
This means your JavaScript can fetch data from api.example.com, but it cannot load scripts or styles from there.
A common pitfall is forgetting to include connect-src. If you have JavaScript making AJAX requests (e.g., using fetch or XMLHttpRequest) to any domain other than your own, and you haven’t explicitly allowed it in connect-src, those requests will fail, and you’ll see connect-src violations in your console.
The report-uri or report-to directive is another crucial, often overlooked, part of CSP. It tells the browser where to send violation reports when a policy is broken. This is invaluable for discovering real-world attacks or misconfigurations without actually breaking your site for users.
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; report-uri /csp-report-endpoint;
You would then need a backend endpoint (like a Netlify Function) at /csp-report-endpoint to receive and log these JSON reports.
Understanding CSP is about moving from a defensive "block everything" mindset to a proactive "allow only what’s necessary" posture. It’s a declarative way to communicate your site’s security assumptions to the browser.
The next logical step after implementing a robust CSP is to consider HTTP Strict Transport Security (HSTS), which forces browsers to always communicate with your site over HTTPS.