- Send Content-Security-Policy-Report-Only first: it generates violation reports without blocking anything, so you learn how a policy would break the site before it does.
- default-src is the fallback for every directive you do not list, which makes default-src 'self' a sound and restrictive starting point.
- Never silence reports with 'unsafe-inline'; use a nonce for server-rendered pages or a hash for static ones, and note that a nonce makes 'unsafe-inline' be ignored.
A Content Security Policy tells the browser which resources a page is allowed
to load. The reason most teams never ship one is the fear of breaking the site on a Friday
afternoon. There is a way around that: send the policy in report-only mode first, read what
it would have blocked, fix those things, and only then enforce. The header is
Content-Security-Policy, its directives are separated by semicolons, and the
report-only variant is a different header entirely.
The one directive to understand first
default-src is the fallback. MDN describes it as setting "a fallback policy
for all resources whose directives are not explicitly listed", so a policy of
default-src 'self' restricts everything to the page's own origin until a more
specific directive says otherwise.
That makes the mental model simple. Start restrictive, then open exactly what you need:
| Policy | Effect |
|---|---|
default-src 'self' | Every resource type must be same-origin |
default-src 'self'; img-src 'self' example.com | Images may also come from example.com; everything else stays same-origin |
default-src 'self'; font-src 'self' fonts.gstatic.com | Web fonts may come from Google's font host; everything else stays same-origin |
The directives you will almost certainly need to name explicitly are
script-src, style-src, img-src,
font-src and connect-src, because analytics, embedded fonts and
API calls are the things a same-origin default breaks first.
Report-only is the whole trick
There are two headers, and the difference is the entire safety mechanism. MDN puts it
plainly: the policy in Content-Security-Policy "is enforced while the
Content-Security-Policy-Report-Only policy generates reports but is not
enforced."
So you can ship a policy that would break your site, learn precisely how it would break
it, and no visitor notices. One constraint is worth knowing before you plan the rollout: a
report-only policy cannot be delivered in a <meta> element, so it has to
come from the server as a real response header.
| Step | Header | What you are doing |
|---|---|---|
| 1 | Content-Security-Policy-Report-Only | Publish your intended policy; nothing is blocked |
| 2 | same | Collect violation reports for a week of real traffic |
| 3 | same | Add the hosts you actually depend on; remove the ones you do not |
| 4 | same | Wait until reports go quiet |
| 5 | Content-Security-Policy | Switch the header name. The policy is unchanged |
Step 4 is the one to be patient about. A week of traffic surfaces the marketing tag that only fires on the pricing page and the embed that only appears in one old blog post.
The two values that undo the work
'unsafe-inline' permits inline scripts, inline event handler attributes and
javascript: URLs. MDN's warning is direct: developers should avoid it "because
it defeats much of the purpose of having a CSP. Inline JavaScript is one of the most common
XSS vectors, and one of the most basic goals of a CSP is to prevent its uncontrolled
use."
'unsafe-eval' permits eval(), the Function()
constructor and string arguments to setTimeout() and
setInterval(), and carries the same advice.
Adding either one to make the reports stop is the most common way a CSP ends up
decorative. If inline scripts are the blocker, the answer is a nonce or a hash, not
'unsafe-inline'.
A detail that saves an afternoon
When a directive contains a nonce or a hash expression, browsers ignore
'unsafe-inline' in that same directive. So a policy carrying both is not
belt-and-braces; the nonce wins and the 'unsafe-inline' does nothing. Worth
knowing before you spend an afternoon wondering why your inline script is still blocked.
Note that 'unsafe-eval' is not disabled the same way.
Nonce or hash: pick by how your pages are built
Both let specific inline scripts run without opening the door to all of them. They suit different architectures.
| Nonce | Hash | |
|---|---|---|
| How it works | Server puts a random value in the header and the same value on the <script> tag; the browser runs the script only if they match | Server publishes the SHA-256, 384 or 512 hash of the script contents, Base64-encoded; the browser hashes the script and compares |
| Hard requirement | The nonce "must be different for every HTTP response, and must not be predictable" | The hash changes whenever the script text changes |
| Best fit | Server-rendered pages that can generate a value per response | Static pages and client-rendered sites, because both the policy and the content can stay fixed |
| External scripts | Nonce on the tag | Also needs the integrity attribute |
The nonce requirement is a real constraint, not a style preference. A nonce reused across responses, or generated from anything guessable, provides no protection at all. If your site is served from a static host or a CDN cache that cannot vary a header per request, hashes are the honest choice.
What to do this week
Pick your highest-traffic page and send
Content-Security-Policy-Report-Only: default-src 'self' with a reporting
endpoint. Do not try to write the perfect policy first. The report will tell you what your
site actually loads, which is almost always a longer list than anyone expects, and that list
is the real starting point.
Frequently asked questions
Can I add a CSP without touching the server?
An enforcing policy can be delivered in a <meta> element, but a
report-only policy cannot. Since report-only is the safe way to start, plan on setting a
real response header.
Will a CSP slow the site down?
It is a response header the browser evaluates as it loads resources, not an extra network
round trip. The practical cost is engineering time spent finding what your pages load.
Is default-src 'self' enough on its own?
It is a sound starting point, and it is the fallback for directives you have not listed. Most
real sites then need to name script-src, style-src,
img-src, font-src or connect-src for the third-party
hosts they genuinely use.
We added a nonce and inline scripts are still blocked. Why?
Check that the nonce on the tag matches the one in the header for that exact response. Also
remember that a nonce in a directive causes 'unsafe-inline' in the same
directive to be ignored, so removing the nonce is not the fix.
Sources
MDN, Content Security Policy guide.
Quoted wording on report-only enforcement, the default-src fallback, the
'unsafe-inline' warning and the nonce uniqueness requirement is MDN's own.