Content Security Policy is one of the most powerful security headers available โ and one of the most fragile. A CSP that protects your users can change or disappear in seconds when you update a CDN rule, rotate a nonce, or modify a reverse proxy config. The effects range from subtle (a third-party widget stops loading) to severe (a broken site that blocks all JavaScript).
This guide covers how CSP regressions happen, how to check your current policy, and how to monitor it automatically so you find out before your users do.
The Content-Security-Policy header tells browsers which sources are allowed to load scripts, styles, images, fonts, and other resources on your page. A strict CSP blocks cross-site scripting (XSS) attacks by refusing to execute inline scripts and restricting external script sources to an explicit allowlist.
A minimal script-blocking CSP looks like this:
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com; style-src 'self' 'unsafe-inline'
This policy says: load everything from the same origin by default, allow scripts only from the current origin and cdn.example.com, and allow inline styles (common requirement for frameworks that inject inline CSS).
When this header disappears or gets weakened โ for example, by someone adding script-src 'unsafe-inline' * to fix a broken widget โ your XSS protection is gone. Browsers won't warn you. Users won't notice unless they're actively exploited.
Cloudflare Transform Rules, AWS CloudFront response headers policies, and Fastly VCL configs all sit in front of your application and can override or strip the CSP your app sends. When a team member updates a CDN rule without realizing it affects security headers, the CSP can change without any application deployment happening.
CSPs that use nonces (script-src 'nonce-abc123') require generating a new nonce on every request. If a deployment changes the nonce generation logic, changes the CSP header string, or accidentally hard-codes a nonce, the policy becomes either broken or trivially bypassable.
Many frameworks set security headers automatically โ Helmet.js, Django's SecurityMiddleware, Rails' secure_headers gem. When you upgrade these packages, the default CSP values often change to reflect new browser support. What was a strict policy on version 4 may become looser on version 6, or stricter in ways that break your legitimate asset loads.
Moving traffic between nginx and a load balancer, adding a new reverse proxy, or changing SSL termination layers can silently drop headers that were configured at a layer that's no longer in the request path. The header was there last Tuesday; it's gone today because the traffic routing changed.
curl -s -I https://yourdomain.com | grep -i "content-security-policy"
If this produces no output, your site has no CSP set. If the output looks different from what you expect, something has changed since you last deployed.
For a full headers check across all six major security headers at once:
curl -s -I https://yourdomain.com | grep -iE "content-security|x-frame|x-content-type|strict-transport|referrer-policy|permissions-policy"
Open Chrome DevTools โ Network tab โ click any document request โ scroll to Response Headers. You'll see the CSP value sent by the server (not necessarily what the meta tag in HTML says โ the HTTP header takes precedence for most enforcement).
The security headers checker shows a full breakdown of your CSP and all other security headers in one check. It flags missing directives, grades your overall security posture, and explains what each header does.
Checking manually is useful for a specific deployment. Monitoring is different: you want to know the moment the header changes on its own โ whether or not a deployment happened.
Save the known-good CSP value to a file, then run a cron job that compares the live header to the saved value:
#!/bin/bash
# /usr/local/bin/check-csp.sh
DOMAIN="yourdomain.com"
EXPECTED_CSP="default-src 'self'; script-src 'self' https://cdn.yourdomain.com"
EMAIL="security@yourdomain.com"
CURRENT_CSP=$(curl -s -I "https://$DOMAIN" | grep -i "content-security-policy" | sed 's/content-security-policy: //i' | tr -d '\r')
if [ "$CURRENT_CSP" != "$EXPECTED_CSP" ]; then
echo "CSP changed on $DOMAIN.
Expected: $EXPECTED_CSP
Got: $CURRENT_CSP" | mail -s "Security alert: CSP changed on $DOMAIN" "$EMAIL"
fi
# crontab: check every 6 hours
0 */6 * * * /usr/local/bin/check-csp.sh
This approach works but requires maintaining the expected value as your CSP legitimately evolves, and it only alerts on exact string differences โ a whitespace change or header case difference triggers a false positive.
name: CSP Monitor
on:
schedule:
- cron: '0 8 * * *' # Daily at 8am UTC
jobs:
check-csp:
runs-on: ubuntu-latest
steps:
- name: Check Content Security Policy
run: |
CSP=$(curl -s -I https://yourdomain.com | grep -i "content-security-policy")
if [ -z "$CSP" ]; then
echo "::error::Content-Security-Policy header is missing!"
exit 1
fi
echo "CSP present: $CSP"
# Optionally: check for dangerous values
if echo "$CSP" | grep -q "unsafe-eval"; then
echo "::warning::CSP contains unsafe-eval"
fi
GitHub will email you when the action fails. This catches a missing CSP but won't alert on a weakened-but-present policy unless you add more specific checks.
The most reliable approach is a service that monitors your security headers continuously from external infrastructure and alerts you the moment any header changes โ including partial changes, weakened values, and header removals.
HeaderGuard checks your HTTP security headers daily and sends an email alert the moment anything changes โ Content-Security-Policy, HSTS, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, and Permissions-Policy.
Add your domain once. Get notified before a security regression turns into a breach report.
Join the HeaderGuard waitlist โ Check your headers free| Directive | Controls | Common safe value |
|---|---|---|
default-src | Fallback for all resource types | 'self' |
script-src | JavaScript sources | 'self' + explicit CDN origins |
style-src | CSS sources | 'self' 'unsafe-inline' (often needed) |
img-src | Image sources | 'self' data: https: |
connect-src | XHR/fetch/WebSocket targets | 'self' + your API origins |
frame-ancestors | Who can embed your page | 'none' or 'self' |
upgrade-insecure-requests | Force HTTPS on mixed content | Include if serving HTTPS |
Values to avoid without understanding them:
'unsafe-inline' in script-src โ eliminates most XSS protection'unsafe-eval' โ allows eval(), required by some legacy code but reduces security* as a source โ allows loading from any originCheck your current CSP values and grade with the free security headers checker โ