# Content Security Policy in WordPress: report first, then decide

Content Security Policy in WordPress without breaking pages: why inline scripts and embeds trip it, why report-only comes first, and how to read reports.

By BetterShield Team, Oct 11, 2026. Updated Nov 8, 2026 by Nazmul Hasan Rupok.

A Content Security Policy in WordPress is a response policy that lists where a page may load scripts, styles, images, fonts and frames from. The browser refuses everything else. It is one of the strongest limits a page can set on itself, and also the header most likely to break the site that sends it.

This guide explains why a policy breaks WordPress sites so easily, why report-only mode comes first, how to read the reports, and exactly what BetterShield does. The [security headers overview](/blog/wordpress-security-headers/) covers the other headers; this one goes deeper on the content policy alone.

## Quick summary

- A content policy tells the browser where a page may load things from, and the browser blocks the rest.
- WordPress pages carry inline scripts and styles from core, the theme and plugins, plus fonts, analytics and embeds from other domains. A strict policy blocks them.
- A report-only policy does not block resources itself. Other enforced policies can still block them.
- BetterShield's **Script-blocking policy** sends a report-only policy on front-end pages and lists what browsers report, by rule and origin.
- **Start enforcing this policy** is offered only after 7 days of watching with an empty list. **Go back to watching** is never held back.

## What a Content Security Policy does

A policy is a list of rules. Each names a kind of content and where it may come from:

| Rule | Covers |
|---|---|
| `default-src` | Fallback for fetch directives without their own source list |
| `script-src` | Scripts |
| `style-src` | Stylesheets and inline styles |
| `img-src` | Images |
| `font-src` | Web fonts |
| `frame-src` | Frames the page shows, such as a video embed |
| `frame-ancestors` | Who may show this page inside a frame |

Sources are written as origins, such as `https://cdn.example.net`, as `'self'` for the page's own origin, or as keywords such as `'unsafe-inline'`, which allows code written straight into the page.

The policy can arrive in two headers. `Content-Security-Policy` enforces it: the browser blocks what it does not allow. `Content-Security-Policy-Report-Only` only watches: the browser loads everything and notes what the policy would have blocked. Either way, if the policy names a report address, the browser sends a short report for each thing it blocked or would have blocked.

## Why a CSP breaks WordPress sites

### Inline scripts and styles

WordPress core, themes and plugins write small scripts and styles straight into the page: settings a script needs, styles for blocks, a snippet in the header. A policy that forbids inline code blocks all of them, and parts of the page quietly stop working.

The careful way to allow inline code is to mark each script with a hash of its contents or a nonce, a random value that changes with every response. On WordPress that is hard. Many separate pieces of code print their own scripts, and a page cache serves the same copy, nonce included, to many visitors. So a WordPress policy usually either allows inline code, which gives up much of the protection against script written into a page, or takes real development work to avoid it.

### Plugins and services that load from elsewhere

A font service, an analytics tag, a cookie banner, a chat widget, images on a CDN: each loads from its own domain, and each needs its own entry in the policy. Install something new next month and the policy needs updating too, or the new feature is blocked on arrival.

### Embeds and avatars

A video or map embedded in a post is a frame from another domain. WordPress loads comment avatars from Gravatar by default. Both need the policy to allow them, or they disappear from the page.

### Pages you rarely open

A policy can look fine on the home page and break the checkout, a form's confirmation step or a page used once a month. The breakage is quiet: a missing button, a form that does nothing. The browser console is often the only place it is written down.

## Report-only first

Report-only mode shows the cost before anyone pays it. Visitors' browsers load everything as usual and report each thing the policy would have blocked. The result is a list of what your pages actually pull in, gathered from real visits to real pages, including the ones you never open yourself.

How long you watch matters as much as watching. An afternoon covers the pages people visited that afternoon. A week covers the weekend visitors, the newsletter traffic, the checkout used twice a week. BetterShield waits a week before it offers to enforce, and says why: "A week covers the traffic a quiet afternoon does not."

## Reading the reports

Each report names the rule and what it would have blocked. Grouped by both and counted, a handful of patterns cover most lines. Browsers name the rule as precisely as they can, so a script line can read `script-src-elem` rather than `script-src`.

| What you see | What it usually points to |
|---|---|
| `img-src` from another host | Images loaded from elsewhere, such as avatars or a CDN |
| `font-src`, or `style-src-elem` from a font host | Web fonts served from another domain |
| `script-src-elem` from another host | A script tag that points at another domain |
| `frame-src` | An embedded video, map or form |
| `connect-src` | A script sending data to another domain |
| `eval` under a script rule | A script that builds code as it runs |

For each line, ask whether the site still needs that thing. If not, remove it at the source: an old tracking tag, a font the theme no longer uses. If it does, a policy for your site has to allow it.

Not every line is about your pages. Reports come from visitors' browsers, and a browser extension can add lines of its own. BetterShield's screen says the same of its list: "Reported by visitors' browsers, so the addresses are theirs rather than this site's."

## BetterShield's flow

The fix is **Script-blocking policy**, under **What browsers are told** on **BetterShield › Protection › Hardening**. It sends `Content-Security-Policy-Report-Only` on front-end pages only. The sign-in page, the REST API and the dashboard never get it.

### The starting policy

| What the page loads | Allowed from |
|---|---|
| Anything not listed below | Your own site |
| Scripts | Your own site, plus inline scripts |
| Styles | Your own site, plus inline styles |
| Images | Your own site, plus `data:` images |
| Fonts | Your own site, plus `data:` fonts |
| Who may show the page in a frame | Your own site |

It allows inline code, so the reports name what your pages load from other addresses, which is the part most site owners do not know. It also means this policy limits where content comes from; it does not stop a script written into the page itself. Every site starts with the same policy. A source your visitors’ browsers reported can be added with **Allow**, on the rule it was reported on, and taken off with **Remove**; additional exceptions cannot enable eval or wildcard sources; inline scripts and styles are already permitted by the baseline.

### Step by step

1. Press **Preview the change**. The note beside the switch says it "starts on the report-only header, so nothing a visitor loads changes."
2. Turn the switch on. The row reads "Watching. Nothing has been reported yet."
3. As reports arrive, the row counts them, as in "3 kinds of thing would have been blocked.", and lists them under **What the policy would have blocked**: the rule, where it came from, and "once" or "4 times", most frequent first. Only the origin is kept, never the full address.
4. **Start enforcing this policy** becomes a button only when the fix has been watching for 7 days and the list is empty. Until then the row gives the reason, such as "Nothing has been reported in 3 of the 7 days this waits for."
5. When it is offered, press it, read "Enforcing sends the same policy for real, so anything it would block stops loading.", and confirm with **Yes: Start enforcing this policy**. The activity log records "Content policy is now enforced", and the same policy now goes out as `Content-Security-Policy`.
6. **Go back to watching** returns to report-only at once. It is never held back by the wait, and the log records "Content policy is back to watching".

Both buttons need a signed-in browser, and ask for your password when **Ask for my password before an action that removes a protection** is on. Turning the fix off and on again starts over in report-only and restarts the 7 days, even if it was enforcing. The list stays through that and is not cleared by time. **Clear this list** empties it, for a cause you have fixed or a report that was never real, and **Put the list back** undoes that. Allowing a source, removing one or clearing the list starts the 7 days again. The [Content security policy](/docs/content-security-policy/) guide covers every line the row can show.

## When enforcing is worth it

Many WordPress sites will see lines on the list. Fix each at its source, or allow it if you recognize it; until the list stays empty for a week, the policy stays in report-only. That is still worth having. The list shows reported violations from the visits observed. It is not a complete dependency inventory: allowed sources do not appear, reports may be missing, and pages that were not exercised contribute nothing. Use it to clear out what you no longer need, and to brief a developer if you want a policy written for your site.

Enforcement is worth considering after required sources have been reviewed, the list stays empty through the waiting period, and representative workflows pass. A site may use approved outside sources; serving everything locally is not the only workable configuration. It adds a limit on where content can come from, and only your own site may show its pages in a frame.

A site that needs outside content can allow each source its visitors’ browsers report, or have a policy written for it at the server or in code. If WordPress or another plugin already sets a `Content-Security-Policy` header, BetterShield leaves it exactly as it is.

## A practical acceptance test before enforcement

Write down the workflows to exercise, not just a list of URLs. A checkout can load its payment script only after a visitor chooses a payment method; a consent banner can load different resources after consent. Test both paths in a fresh browser.

| Journey | Evidence to collect |
|---|---|
| Home page and a normal article | Expected images, fonts and navigation work; the final document carries the intended policy. |
| Search, form and confirmation | The request completes and the success message appears. A clickable button alone proves little. |
| Cart and checkout, where present | Use the store's test-payment mode and complete the flow without placing an unintended real order. |
| Embeds and consent choices | Exercise each supported state, including the resource that appears only after consent. |

**Worked example (fictional):** a form fails only after submission and the console names a `connect-src` violation. The form's JavaScript may load successfully while its network request is blocked. Identify the documented service origin and confirm why it is needed before allowing it; adding only a `script-src` exception would not resolve this particular boundary.

An empty report list can mean no relevant visit occurred or a report never arrived. The seven-day wait is a product prerequisite, not test coverage. Check browser errors and actual outcomes as well as reports. After enforcement, repeat the journeys. If returning to report-only, also verify caches have stopped serving the enforced header; already-open pages retain the policy they received until reloaded.

## Common mistakes

- **Enforcing after a quiet afternoon.** A week of real traffic finds the pages an afternoon misses.
- **Testing only the pages you visit.** Checkouts, confirmations and monthly pages break first and are noticed last.
- **Treating every line as a fault.** Some come from visitors' browser extensions, not your site.
- **Allowing everything to make the reports stop.** A wildcard that silences the list also removes most of what the policy was for.
- **Expecting it to cover the dashboard.** BetterShield's policy is sent on front-end pages only, never on the sign-in page, the dashboard or the REST API.

## Frequently asked questions

### Does report-only mode change anything for visitors?

The report-only policy does not block content itself; browsers can send reports in the background. Other enforced policies or browser controls can still block resources.

### Can I edit BetterShield's policy?

Partly. There are no fields to type into, but a source that visitors’ browsers reported can be allowed with **Allow** and taken off with **Remove**, each with an undo. Additional exceptions cannot enable eval or wildcard sources. The starting policy already permits inline scripts and styles. A site that needs more than that needs its own policy.

### Does the policy cover the sign-in page and the dashboard?

No. It is sent on front-end pages only. The sign-in page, the REST API and the dashboard never get it.

### Why does BetterShield keep only the origin of a blocked address?

A full address can carry details in its query string, and the reports come from visitors. The origin, such as `https://cdn.example.net`, is enough to tell you what to look at.

### How do I stop enforcing?

Press **Go back to watching** and confirm. New responses return to report-only. Refresh affected pages and check any page cache or CDN; another enforced policy can still block resources.

## Conclusion

A Content Security Policy in WordPress is worth understanding before it is worth enforcing. Inline code, outside services, embeds and rarely visited pages all collide with a strict policy, so watch first, read what browsers report over a full week, and enforce only when the list stays empty.

BetterShield is free on [WordPress.org](https://wordpress.org/plugins/bettershield/). The [Content security policy](/docs/content-security-policy/) guide walks through the fix, its list and the enforce step.
