Support Library
Content-Security-Policy: Stopping Injected Scripts
Content-Security-Policy (CSP) is a response header that tells the browser which scripts a page is allowed to run. When an attacker manages to inject a script, through a comment field, a vulnerable plugin or a compromised third party, a good policy stops the browser from running it.
The short answer
Send a Content-Security-Policy that only allows the scripts you intend to run. The most robust form uses a random nonce on each page:
Content-Security-Policy: script-src 'nonce-R4nd0m' 'strict-dynamic'; object-src 'none'; base-uri 'none'Your server generates a new nonce for every response and adds nonce="R4nd0m" to each legitimate <script> tag. An injected script does not know the nonce, so it is blocked. Roll it out in report-only mode first, then enforce it.
What a policy stops
This kind of attack is called cross-site scripting (XSS), and it has been on the OWASP Top 10 list of web application risks for two decades. Escaping user input is the first defence; CSP limits the damage when something is missed.
How DNS Panopticon detects it
Security Posture reads the Content-Security-Policy header on your homepage and works out whether an injected script could still run. It looks at script-src-elem, script-src-attr, script-src and default-src, in the order browsers fall back through them. When several policies are sent, a script must pass all of them. The result appears on the Website Application Security card.
| Finding | Severity | When it appears |
|---|---|---|
| No Content-Security-Policy | Medium | No enforced policy. A Content-Security-Policy-Report-Only header alone is noted, since it reports but does not block. |
| Content-Security-Policy allows any script | Low | The policy allows 'unsafe-inline' without a nonce, hash or 'strict-dynamic', or allows scripts from any site through a source such as *, https: or http:. |
| Content-Security-Policy is set | Pass | The policy limits which scripts may run. |
A <meta http-equiv> policy in the page HTML is not read; send the policy as a header.
How to check it yourself
| Command or tool | What to look for |
|---|---|
| curl -sI https://example.com | A content-security-policy line. Check script-src (or default-src if there is none) for 'unsafe-inline', * or https:. |
| https://csp-evaluator.withgoogle.com/ | Paste the policy into Google’s CSP Evaluator for a directive-by-directive review. |
In the browser, open developer tools. The Console lists every resource the policy blocked, with the directive that blocked it, which is the quickest way to tune a policy.
Why a weak policy is nearly as bad as none
- 'unsafe-inline' allows any inline <script> block or onclick handler, which is exactly what most injected code looks like. A nonce or hash disables it in browsers that support them.
- * or https: allows scripts from any website, so an attacker can host their script anywhere and point to it.
- Broad allowlists of big CDNs can be bypassed when those CDNs host old libraries or JSONP endpoints that run attacker-chosen code. Nonces with 'strict-dynamic' avoid that problem.
How to fix it
- Inventory your scripts. List first-party scripts, inline scripts and third-party tags (analytics, chat, tag managers).
- Start in report-only mode. Send the policy as Content-Security-Policy-Report-Only with a report-to or report-uri endpoint and watch what would have been blocked.
- Add nonces or hashes. Generate a fresh random nonce per response and put it on every legitimate script tag. For static sites, list SHA-256 hashes of inline scripts instead. Move inline event handlers such as onclick into script files.
- Enforce it. Switch the header name to Content-Security-Policy once reports are quiet.
- Add the useful extras: object-src 'none', base-uri 'none', frame-ancestors 'self' (see clickjacking) and upgrade-insecure-requests (see mixed content).
| Approach | Example script-src | Good for |
|---|---|---|
| Nonce-based (strict) | 'nonce-{random}' 'strict-dynamic' | Sites rendered by a server or framework that can add a nonce to each page |
| Hash-based (strict) | 'sha256-…' 'strict-dynamic' | Static sites with a few fixed inline scripts |
| Allowlist | 'self' https://cdn.example.net | Simple sites with no inline scripts; easier, but weaker |
Most frameworks can add nonces for you: for example Next.js middleware, Django’s django-csp package, Helmet for Express, or NWebsec for ASP.NET. CMS platforms often have a plugin, though many start with an allowlist that still includes 'unsafe-inline'; tighten it once the site works.
FAQ
Does CSP replace fixing XSS bugs?
No. Escape and validate user input first. CSP is defence in depth: it stops many injected scripts from running when a bug slips through.
Why does the policy break my analytics or tag manager?
Those tools load scripts from their own domains and often inject inline code. With 'strict-dynamic', scripts loaded by a trusted, nonced script are allowed, which covers most tag managers. Check the vendor’s CSP guide for anything else it needs.
Is a report-only policy enough?
No. Report-only shows what would be blocked but blocks nothing, which is why DNS Panopticon still treats it as missing. Use it while you tune the policy, then enforce.
Should I use a meta tag instead of a header?
Use the header where you can. A meta tag cannot set frame-ancestors or reporting, and only applies after the browser reaches it in the HTML.