You've built your site, it looks great and everything works. But have you thought about what happens if someone manages to inject a script on your page? Content Security Policy, or CSP, is the web's firewall against exactly that — and it's easier to get started with than you think.
What is Content Security Policy?
Content Security Policy is an HTTP header that tells the browser exactly which resources your page is allowed to load. This covers scripts, stylesheets, images, fonts, iframes and much more. By setting a CSP policy, you decide which domains are allowed — everything else is blocked.
The basic idea is simple: instead of trying to find and filter out dangerous code (which always has gaps), you simply tell the browser to only run code from approved sources.
Why do you need CSP?
The most common attack CSP protects against is Cross-Site Scripting (XSS). XSS means an attacker manages to inject JavaScript on your page — via a form field, a URL parameter or a compromised third-party service. Without CSP, the browser happily runs all JavaScript that appears in the DOM, regardless of where it comes from.
With a correct CSP header, the browser sees that the script doesn't come from an allowed source and refuses to run it. The attack is stopped before it even starts.
CSP also helps against:
- Clickjacking — by controlling which pages can embed your site via iframes
- Data exfiltration — by restricting which domains your page can send data to
- Mixed content — by enforcing HTTPS for all resources
What does a CSP header look like?
A CSP policy is sent as an HTTP header and consists of directives that control different resource types. Here's a simple example:
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com; style-src 'self' 'unsafe-inline'; img-src *;
This says:
| Directive | What it does |
|---|---|
default-src 'self' | By default, only load resources from the site's own domain |
script-src 'self' https://cdn.example.com | JavaScript may only be loaded from the site's own domain and the CDN |
style-src 'self' 'unsafe-inline' | CSS from the site's own domain, plus inline styles (often needed in practice) |
img-src * | Images may be loaded from any domain |
The most important directives
There are a bunch of directives to choose from. Here are the ones you should know:
| Directive | Controls | Common setting |
|---|---|---|
default-src | Fallback for all resource types | 'self' |
script-src | JavaScript | 'self' |
style-src | CSS | 'self' 'unsafe-inline' |
img-src | Images | 'self' data: |
font-src | Fonts | 'self' https://fonts.gstatic.com |
connect-src | API calls, WebSockets | 'self' |
frame-src | Iframes | 'none' |
object-src | Plugins (Flash, Java) | 'none' |
base-uri | What the <base> tag may point to | 'self' |
form-action | Where forms may be submitted | 'self' |
Report-Only: test without breaking anything
The biggest obstacle to implementing CSP is the fear that something will stop working. And sure, if you set a policy that's too strict, scripts and stylesheets can be blocked. But there's a solution: Content-Security-Policy-Report-Only.
Instead of blocking, the browser only reports violations. You can send the reports to an endpoint and analyze them at your leisure:
Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-report;
Run Report-Only for a couple of weeks, check the logs, adjust the policy and activate it for real when you know everything works. This is the safe way to roll out CSP without risking downtime.
Nonce and hash — the modern way
Allowing 'unsafe-inline' for scripts is like putting a lock on the door but leaving the window open. It opens up for XSS again. The modern solution is called nonce or hash.
A nonce (number used once) is a random value generated with each page load. You put it in the header and on your script tags:
Content-Security-Policy: script-src 'nonce-abc123'
<script nonce="abc123">
// This script runs
</script>
<script>
// This gets blocked — no nonce
</script>
With nonce you can have inline scripts without opening up for XSS. Each page load generates a new nonce, so an attacker cannot guess the right value.
CSP in practice — Apache and Nginx
Setting the header is trivial in most web servers.
Apache (in .htaccess or virtualhost):
Header set Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; object-src 'none'; frame-ancestors 'none';"
Nginx:
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; object-src 'none'; frame-ancestors 'none';" always;
PHP (in your application):
header("Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline';");
Sitewide vs. global configuration — where should the CSP header go?
This is a crucial detail that is often overlooked: where you place your CSP header matters a lot, especially if you run multiple sites on the same server.
There are three levels to choose from:
| Level | File / Location | Affects |
|---|---|---|
| Global | apache2.conf, httpd.conf or Nginx nginx.conf / http {} block | All sites on the entire server |
| Sitewide (vhost) | Apache: sites-available/example.confNginx: conf.d/example.conf or sites-available/ | Only the specific site/domain |
| Sitewide (.htaccess) | .htaccess in the site root (Apache only) | Only that directory and subdirectories |
Global configuration
If you place CSP in the global Apache or Nginx configuration, it applies to all sites on the server. This can be right if you run a server with a single site, but it quickly becomes problematic if you have multiple domains with different needs. One site might use Google Analytics and another uses an external chat widget — they need completely different CSP rules.
# Apache — global (affects ALL sites)
# /etc/apache2/apache2.conf
Header set Content-Security-Policy "default-src 'self';"
# Nginx — global (affects ALL sites)
# /etc/nginx/nginx.conf → http { }
add_header Content-Security-Policy "default-src 'self';" always;
Virtualhost configuration (recommended)
The best location in most cases is in the virtualhost file for the specific site. Then each domain gets its own policy and you don't risk a change for one site breaking another.
# Apache — per site
# /etc/apache2/sites-available/example.com.conf
<VirtualHost *:443>
ServerName example.com
Header set Content-Security-Policy "default-src 'self'; script-src 'self' https://analytics.example.com;"
</VirtualHost>
# Nginx — per site
# /etc/nginx/sites-available/example.com
server {
server_name example.com;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' https://analytics.example.com;" always;
}
.htaccess — flexible but slower
.htaccess only works in Apache and is read on every request, making it marginally slower. The advantage is that you can change CSP without reloading the web server, and it lives in the site's directory rather than in the server configuration.
# /var/www/example.com/.htaccess
Header set Content-Security-Policy "default-src 'self'; script-src 'self';"
Why it matters — especially with AI-assisted development
If you use AI tools (like GitHub Copilot, Claude or ChatGPT) to configure your server, it is critical that you specify exactly where the change should be made. If you ask an AI to "add CSP headers" without specifying where, it might edit the global configuration — and suddenly all sites on the server have a new policy that might break third-party integrations.
A good rule of thumb:
- Always specify which site it concerns — "Add CSP to the vhost file for example.com", not just "Add CSP"
- Point to the right file — specify the file path, e.g.
/etc/apache2/sites-available/example.com.conf - Avoid global changes unless you deliberately want the same policy on all sites
- Always review the diff — verify that the change ended up in the right place before applying it
This applies not only to CSP but to all HTTP headers and server configurations. But CSP is extra sensitive because an incorrect policy can directly knock out functionality on the site.
Common mistakes to avoid
- Using
unsafe-inlineandunsafe-evalin script-src — this makes the entire CSP protection against XSS ineffective - Allowing overly broad domains —
script-src https:allows scripts from any HTTPS domain, including the attacker's - Forgetting
default-src— without a fallback, resources can be loaded without restriction - Not testing with Report-Only first — you risk breaking third-party services like analytics, chat widgets and payment solutions
- Setting CSP and then forgetting about it — your site changes, and the policy must be updated in pace with new integrations
A good starter policy
If you've never used CSP before, start with this:
Content-Security-Policy-Report-Only:
default-src 'self';
script-src 'self';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
font-src 'self';
connect-src 'self';
frame-ancestors 'none';
object-src 'none';
base-uri 'self';
form-action 'self';
report-uri /csp-report;
Run it in Report-Only mode, analyze the reports and add the domains you actually need. Then switch to the live header Content-Security-Policy.
Summary
CSP is one of the most powerful tools you have to protect your website. It requires a bit of planning — you need to know which resources your site loads — but the payoff is enormous. A well-configured CSP policy:
- Stops XSS attacks before they reach the user's browser
- Limits the damage if a third-party service is compromised
- Gives you control over exactly which resources your site may use
- Raises your security profile in tools like Mozilla Observatory and SecurityHeaders.com
Need help with web security?
We help you configure CSP and other security headers — correctly from the start, without breaking your site. Whether you run Apache, Nginx or PHP, we can set up a policy that protects your visitors and raises your security profile.
Contact us