Medium risk Website security header
Missing Content-Security-Policy header: what it means and how to fix it
Short answer: a Content-Security-Policy (CSP) header tells browsers which places your website may load scripts and other content from. Without one, if an attacker manages to slip a malicious script into a page, through a form, a comment or a hacked plugin, the browser will run it for every visitor.
The fix: add a CSP header, starting in "report only" mode so nothing breaks while you tune it. A safe first policy:
Content-Security-Policy: default-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'self'
What CSP protects against
The attack CSP is built for is cross-site scripting (XSS). An attacker gets their own JavaScript to run on your site. That script can steal logins and session cookies, change what customers see, add a fake payment form, or send visitors elsewhere.
CSP is a second layer of defense. Your site should still validate and escape anything users type in. But if a bug or a compromised plugin lets a script through, a good CSP stops the browser from loading it from an attacker's server or running it inline.
The frame-ancestors part of CSP also stops other sites from showing your pages inside an invisible frame to trick visitors into clicking things. This is called clickjacking.
How serious is it?
A missing CSP isn't a vulnerability by itself. It means one important safety net is missing. FreshScanSentry rates it Medium, as OWASP ZAP does, because it makes any XSS bug much more damaging. It matters most for sites with logins, checkouts, customer data or admin areas.
- 443 Content Security Policy (CSP) Header Not Set Found on 10 URLs Medium
- 443 Missing Anti-clickjacking Header Fixed by frame-ancestors Medium
How to check your site
curl -sI https://your-site.com | grep -i content-security-policy
No output means there's no CSP header. Some sites set CSP in a <meta http-equiv="Content-Security-Policy"> tag instead. That works for most rules, but not frame-ancestors or reporting, so a header is better.
How to add a CSP without breaking your site
1. List what your site loads from elsewhere
Open your site with the browser's developer tools on the Network tab and note every outside domain: analytics, tag managers, fonts, chat widgets, payment forms, embedded videos, maps. Each one needs to be allowed in the right directive:
| Directive | Controls | Example |
|---|---|---|
default-src | Fallback for everything not listed | 'self' |
script-src | JavaScript | 'self' https://www.googletagmanager.com |
style-src | Stylesheets | 'self' https://fonts.googleapis.com |
font-src | Web fonts | 'self' https://fonts.gstatic.com |
img-src | Images | 'self' data: https: |
connect-src | Background requests (APIs, analytics) | 'self' https://*.google-analytics.com |
frame-src | Things you embed (videos, payment forms) | https://www.youtube.com https://js.stripe.com |
frame-ancestors | Who may embed your pages | 'self' or 'none' |
2. Start in report-only mode
Send your policy as Content-Security-Policy-Report-Only first. Browsers then report what the policy would block, in the developer tools console, without blocking anything. Browse every important page, including checkout, login, contact forms and the admin area, and add any missing sources.
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self' https://www.googletagmanager.com; img-src 'self' data: https:; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'self'
3. Add the header to your server
Use Content-Security-Policy-Report-Only while testing, then switch the name to Content-Security-Policy to enforce it.
Apache (needs mod_headers):
Header always set Content-Security-Policy "default-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'self'"
nginx (in the server block):
add_header Content-Security-Policy "default-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'self'" always;
IIS. In IIS Manager, open HTTP Response Headers for the site, choose Add, and enter Content-Security-Policy as the name and your policy as the value. Or add it under <customHeaders> in web.config.
Cloudflare. Go to Rules → Transform Rules → Modify Response Header, create a rule for your site, and Set static a header named Content-Security-Policy with your policy.
Node.js / Express with helmet:
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
objectSrc: ["'none'"],
baseUri: ["'self'"],
formAction: ["'self'"],
frameAncestors: ["'self'"],
},
}));
4. Enforce it, then tighten over time
When report-only mode shows nothing important being blocked, switch to the enforcing header and check the site again. Then work toward removing 'unsafe-inline' and 'unsafe-eval' from script-src if you had to add them. Those two are what let injected scripts run. Modern sites replace them with a per-page nonce or hashes for the inline scripts they really need.
For comparison, this site's own policy is default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; frame-ancestors 'none' and a few more rules. Scripts only load from our own domain. Inline styles are allowed because they can't run code.
If the scan says your CSP is weak
OWASP ZAP also flags policies that exist but leave gaps. Common ones:
- "CSP: script-src unsafe-inline" or "unsafe-eval": inline or eval'd scripts are allowed, which is what most XSS attacks rely on. Move inline scripts into files, or use nonces or hashes.
- "CSP: Wildcard Directive": a directive allows
*or a whole scheme likehttps:for scripts. List specific domains instead. A wildcard inimg-srcis usually fine. - "CSP: Failure to Define Directive with No Fallback": something like
frame-ancestorsorform-actionis missing. These don't fall back todefault-src, so set them explicitly.
Google's CSP Evaluator checks a policy and explains each weakness.
While you're there: two more headers
Scanners usually flag these alongside CSP. Each is a single line.
X-Content-Type-Options: nosniffstops browsers from guessing a file's type, which attackers can abuse to make an uploaded file run as a script. Always safe to add. It fixes "X-Content-Type-Options Header Missing".X-Frame-Options: SAMEORIGIN(orDENY) is the older version offrame-ancestors, still worth sending for older browsers. It fixes "Missing Anti-clickjacking Header" and "Missing X-Frame-Options header".
# Apache
Header always set X-Content-Type-Options "nosniff"
Header always set X-Frame-Options "SAMEORIGIN"
# nginx
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
Common questions
We use WordPress with lots of plugins. Is CSP realistic?
Yes, if you start small. object-src 'none'; base-uri 'self'; frame-ancestors 'self' almost never breaks anything and already fixes the clickjacking finding. Plugins often add inline scripts, so a strict script-src takes more testing in report-only mode. Some security plugins can manage CSP for you.
Why do I see "Refused to load…" errors after adding CSP?
The policy is blocking something your page uses. The error names the blocked address and the directive. Add that domain to the directive if you trust it, or remove the thing loading it.
Does CSP replace fixing XSS bugs?
No. It limits the damage if one gets through. Keep plugins, themes and frameworks updated, and escape user input.
Sources: MDN: Content Security Policy · W3C CSP Level 3 · OWASP CSP cheat sheet · ZAP alert: CSP Header Not Set · Google CSP Evaluator