← All guides
High Reported as: Content-Security-Policy present

Missing Content-Security-Policy

What a Content-Security-Policy header does, why SecScan reports its absence as High, and how to ship one without breaking your app.

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:

FindingSeverityMeaning
No Content-Security-PolicyHighNo enforced policy in a header or in a <meta http-equiv> tag
script-src contains a bare *HighScripts may load from any origin
Policy is report-onlyMediumIt reports violations and blocks nothing
script-src allows unsafe-inline or unsafe-evalMediumInjected inline script or eval() still runs
No object-src 'none'MediumPlugin content is unrestricted
No base-uriLowAn injected <base> tag can redirect relative URLs
style-src allows unsafe-inlineLowInjected 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