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
Three separate findings come out of one header:
| Check | Severity | Meaning |
|---|---|---|
Content-Security-Policy present | High | No policy at all |
Policy contains unsafe-inline | Medium | Inline <script> still executes |
Policy contains unsafe-eval | Medium | eval() and new Function() still execute |
The two unsafe-* findings matter because a policy carrying both 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.
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 reports both because the older header is still what
some scanners and compliance checklists look for, and costs nothing to send.
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-07