BetterShield
Appearance
Content Security PolicySecurity headersHardening

Content Security Policy in WordPress: report first, then decide

· · 12 min read

Updated by Nazmul Hasan Rupok on

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 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:

RuleCovers
default-srcFallback for fetch directives without their own source list
script-srcScripts
style-srcStylesheets and inline styles
img-srcImages
font-srcWeb fonts
frame-srcFrames the page shows, such as a video embed
frame-ancestorsWho 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.”

From reports to a decision

A quiet list needs a tested site

The waiting period is one checkpoint, not the whole acceptance test.

  1. Collect representative traffic

    Visit real workflows.

  2. Explain each source

    Keep, remove or investigate.

  3. Enforce and recheck

    Verify successful outcomes.

Collect representative traffic

What this means
Reports describe paths that actually ran and reached the reporting endpoint. Rare pages, blocked reports and cached responses can leave gaps.
Your next check
Test search, consent choices, checkout and confirmations in a fresh browser.

Explain each source

What this means
An unfamiliar origin might be a needed integration, an old tag or browser-added content. A frequent report is not automatic permission to allow it.
Your next check
Find the responsible feature and verify its expected origin before adding an exception.

Enforce and recheck

What this means
After eligibility, enforce during a window when someone can watch key workflows and return to report-only if needed.
Your next check
Confirm forms submit, transactions complete and the intended response header reaches visitors.
A seven-day empty list does not prove that every page was exercised. Clearing reports is not a substitute for resolving their cause.

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 seeWhat it usually points to
img-src from another hostImages loaded from elsewhere, such as avatars or a CDN
font-src, or style-src-elem from a font hostWeb fonts served from another domain
script-src-elem from another hostA script tag that points at another domain
frame-srcAn embedded video, map or form
connect-srcA script sending data to another domain
eval under a script ruleA 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 loadsAllowed from
Anything not listed belowYour own site
ScriptsYour own site, plus inline scripts
StylesYour own site, plus inline styles
ImagesYour own site, plus data: images
FontsYour own site, plus data: fonts
Who may show the page in a frameYour 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 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.

JourneyEvidence to collect
Home page and a normal articleExpected images, fonts and navigation work; the final document carries the intended policy.
Search, form and confirmationThe request completes and the success message appears. A clickable button alone proves little.
Cart and checkout, where presentUse the store’s test-payment mode and complete the flow without placing an unintended real order.
Embeds and consent choicesExercise 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. The Content security policy guide walks through the fix, its list and the enforce step.

Close the open doors today

Install the free plugin. The first audit runs when you activate it, and all but one of the fixes wait for your choice.

Requires WordPress 6.7 or newer and PHP 8.0 or newer.

Get BetterShield