A Content-Security-Policy (CSP) tells the browser which sources it is allowed to
load code from. Without one, any HTML injection anywhere in your application
becomes script execution: the browser has no reason to refuse a <script> tag
just because it was not supposed to be there.
This is why the check is graded High rather than Medium. A missing CSP is not itself a vulnerability — it is the absence of the control that would have contained one.
What SecScan checks
Seven separate findings come out of one policy:
| Finding | Severity | Meaning |
|---|---|---|
| No Content-Security-Policy | High | No enforced policy in a header or in a <meta http-equiv> tag |
script-src contains a bare * | High | Scripts may load from any origin |
| Policy is report-only | Medium | It reports violations and blocks nothing |
script-src allows unsafe-inline or unsafe-eval | Medium | Injected inline script or eval() still runs |
No object-src 'none' | Medium | Plugin content is unrestricted |
No base-uri | Low | An injected <base> tag can redirect relative URLs |
style-src allows unsafe-inline | Low | Injected styles still apply |
A policy delivered in a <meta http-equiv="Content-Security-Policy"> tag counts as
the site’s policy, except that a meta tag cannot carry frame-ancestors, so that
directive is only read from the header. A Report-Only tag is not a policy.
The unsafe-* finding matters because a policy carrying it blocks very
little of what an injected payload actually does. Shipping
script-src 'self' 'unsafe-inline' and calling it done is the most common way to
pass a header checklist while leaving the hole open. It is not raised when a nonce,
a hash or 'strict-dynamic' is also present, because browsers then ignore
unsafe-inline; and 'wasm-unsafe-eval' is not unsafe-eval. A wildcard on a
named domain, such as https://*.example.com, is an allowlist entry, not the
bare *.
How to fix it
Start in report-only mode. Content-Security-Policy-Report-Only sends violation
reports without blocking anything, so you can see what a policy would break
before it breaks it for real users:
Content-Security-Policy-Report-Only:
default-src 'self';
script-src 'self';
style-src 'self';
img-src 'self' data:;
frame-ancestors 'none';
base-uri 'self';
report-uri /csp-report
Collect reports for a week of real traffic. Every violation is either something
you need to allowlist or something you should not have been loading. When the
report volume settles, rename the header to Content-Security-Policy.
Getting rid of unsafe-inline
Inline scripts are the usual reason a policy stalls at unsafe-inline. Two ways
out:
- Nonces. Generate a random value per response, put it on the header
(
script-src 'nonce-r4nd0m') and on each legitimate inline tag (<script nonce="r4nd0m">). The nonce must be unpredictable and must change every response — a static nonce is the same as no nonce. - Hashes. For inline blocks that never change,
script-src 'sha256-…'pins the exact content. No per-request work, but every edit to the block changes the hash.
Note that unsafe-inline is ignored by browsers when a nonce or hash is also
present, so you can leave it in place as a fallback for very old clients without
weakening modern ones.
frame-ancestors replaces X-Frame-Options
If you set frame-ancestors, it supersedes X-Frame-Options in every browser
that supports CSP. SecScan raises the clickjacking finding only when neither is
present, so either one satisfies it. Sending both costs nothing and covers the
scanners and checklists that still look for the older header.
Verify after deploying. A CSP set by your application can be silently overwritten by a CDN or reverse proxy in front of it. Re-run the scan against the public URL, not localhost.
Last reviewed 2026-09-30