Skip to content
pageinspection

A content security policy, and the honest cost of one

A content security policy is the strongest thing you can do about injected scripts and the fastest way to take your own site down. Both halves of that are true, which is why the order you introduce it in matters more than the policy you end up with.

The short answer

Content-Security-Policy tells the browser which sources may load scripts, styles and other resources, which is what turns an injected script from an exploit into a blocked request. Deploy it in report-only mode first and leave it there longer than you think, because a policy that blocks your own analytics is discovered by your users.

ImportantAudit check · CSP headers

What a policy actually prevents

A policy is a list of the sources a page is allowed to load things from, enforced by the browser. If an attacker manages to inject a <script src> pointing at their own host, or an inline script into a comment field, the browser refuses to run it because it is not on the list.

That is the defence, and its shape is worth understanding: a policy does not stop an injection, it stops the injected thing from being useful. Which means it is a second line rather than a first, and it is valuable precisely because your first line, escaping everything correctly everywhere forever, is not a thing anybody achieves.

The directives worth knowing before writing one:

policy.txt
default-src 'self'          the fallback for everything not named below
script-src 'self'           where scripts may come from. the important one
style-src 'self'            stylesheets
img-src 'self' data:        images, plus inline data URIs
font-src 'self'             web fonts
connect-src 'self'          where fetch and XHR may go
frame-ancestors 'none'      who may put this page in an iframe
form-action 'self'          where a form may submit to
base-uri 'self'             stops an injected <base> rewriting every URL
object-src 'none'           plugins. almost always safe to forbid outright
upgrade-insecure-requests   rewrite http references to https before request
report-uri /csp-report      where violation reports are sent

Two of those are cheap and frequently forgotten. base-uri closes an injection that silently rewrites every relative URL on your page, and object-src 'none' costs nothing on any modern site. And upgrade-insecure-requests is the single directive that resolves most of a mixed content problem without touching your content.

Report-only first, and for longer than you think

Content-Security-Policy-Report-Only is the same header with the enforcement removed. The browser evaluates the policy, blocks nothing, and sends you a report for every violation. This is how a policy gets introduced without an outage, and the mistake is not skipping it, it is running it for two days.

Two weeks is a better minimum, and the reason is coverage rather than caution. Your reports only cover pages people visited with browsers they used. A month of reports catches the checkout flow somebody uses on the last day of the month, the admin screen used quarterly, the marketing tag that only fires on one campaign page, and the embed on the page nobody remembers owning.

What to expect in those reports, in order of volume:

  • Browser extensions. The largest source of noise by far, and none of it is your problem. Violations from extension schemes can be discarded wholesale.
  • Your own analytics and tag manager. A tag manager is a mechanism for injecting arbitrary scripts, which is exactly what a policy exists to stop. The two are in genuine tension, and resolving it means either enumerating what the tag manager loads or accepting a much weaker script-src.
  • Inline styles from your framework. Component libraries that set styles inline will violate a strict style-src. This is common enough that many policies allow inline styles and forbid inline scripts, which is a defensible trade.
  • One thing you had forgotten about entirely. There is always one. A font service, an old chat widget, a script from an agency engagement that ended three years ago.

Do not reach for unsafe-inline on scripts to make the reports stop.

Allowing inline scripts removes the protection the policy was for, because an injected inline script is the most common form of the attack. If you have inline scripts you cannot remove, the tools are a nonce or a hash: a per-response random value on the tag and in the header, or the hash of the script's contents. Both are more work than a keyword and both keep the policy meaningful.

A policy with unsafe-inline in script-src is worth keeping for its other directives, and it is worth being honest with yourself that the headline protection is not switched on.

A header, and occasionally a meta tag

A policy normally arrives as a response header, which is not part of a payload assembled from page content. It can also be delivered as a <meta http-equiv> tag, and that form is genuinely weaker: it cannot carry frame-ancestors or the reporting directives, and it applies only from the point in the document where it appears.

This one needs the full crawl

A policy arrives as a response header, or occasionally as a meta tag, and the header form is invisible to a payload built from page content.

The instant search on this site audits a single page, so rather than show you a verdict it cannot support, this guide sends you to the place the check actually runs.

What the audit exports for this one

This is one of two checks in the catalogue where the fix export is not a code change. There is nothing in your repository to edit, because the answer depends on a stack the crawler cannot see: the same missing policy is a server configuration change, a CDN rule, a middleware function or a meta tag depending on how you are hosted.

So the brief gives both routes and says which is which. Below is the real file, exactly as the API produced it against a crawl.

fix-missing-csp.md
# Fix: Missing Content-Security-Policy Header

**Site:** https://example.com

**Check:** `sec-missing-csp` · **Severity:** info · **Category:** SEO · **Affected pages:** 1

## What's wrong
The page at https://example.com/ does not send a Content-Security-Policy header, which is a crucial security feature to prevent cross-site scripting (XSS) and resource-injection attacks. The audit found that the page has no Content-Security-Policy header, leaving it vulnerable to these types of attacks. This was detected by the absence of the header in the page's HTTP response.

## Why it matters
The absence of a Content-Security-Policy header makes the site more susceptible to XSS and resource-injection attacks, which can compromise user data and site integrity. Search engines and AI answer engines may also view the site as less secure, potentially impacting its visibility and credibility. This can lead to a loss of user trust and potential security breaches.

## How to fix
1. Search for the HTML `<head>` tag or the template helper that generates it, as this is where the Content-Security-Policy header is typically defined.
2. Add a `<meta>` tag with the `http-equiv` attribute set to "Content-Security-Policy" and the `content` attribute set to a policy that defines the allowed sources for the site's resources, such as `default-src 'self'; script-src 'self' https://cdn.example.com;`.
3. Ensure the final HTML output includes a `<meta>` tag similar to: `<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' https://cdn.example.com;">`.
4. Alternatively, configure the web server to send a Content-Security-Policy header in the HTTP response, with a policy that defines the allowed sources for the site's resources, such as `Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com;`.

## Acceptance criteria
- [ ] The page at https://example.com/ sends a Content-Security-Policy header in its HTTP response.
- [ ] The Content-Security-Policy header defines a policy that allows resources to be loaded from the site's own domain ('self') and any other necessary domains (e.g., CDNs).
- [ ] The page's HTML does not contain any inline scripts or styles that are not explicitly allowed by the Content-Security-Policy.

## Affected pages

| # | URL | Observed value |
| --- | --- | --- |
| 1 | `https://example.com/` | `No Content-Security-Policy header` |

## Ground rules for the agent

- Start by identifying the stack yourself (package manifest, config files, template extensions) and adapt every step below to it. The steps describe the required output, not the framework.
- Treat the URLs and observed values above as facts from a real crawl. Do not invent additional affected pages.
- Prefer fixing the shared template or layout over patching one page, then confirm the change reaches every affected URL listed.
- Change content and templates, not URLs — renaming a route creates redirects and breaks inbound links.
- If a fix needs copy written (titles, descriptions), draft it from the page's existing content rather than inventing claims about the product.
- Make one issue-type change per commit so a regression can be bisected.
- Do not mark an item complete until its acceptance criterion is actually verifiable.

Worth reading with one correction in mind, and it is a useful illustration of what these exports are for. The brief leads with the meta tag route, because that is the change an agent can make inside a codebase without knowing anything about your infrastructure. It is the weaker of the two for the reasons in the previous section, so if you have access to your response headers, take the header route and treat the meta tag as the fallback the brief offers for when you do not.

Why a security header appears in an audit at all

A reasonable objection: a content security policy has nothing to do with search, so what is it doing in a catalogue of SEO checks. Two answers, and the first is about scope.

The audit reports what is true about a page, and the things that are true about a page cross categories. A missing policy is a fact about how the site is operated, discoverable in the same request as everything else, and useful to whoever reads the report. Excluding it because of a taxonomy would be withholding a finding to keep a category tidy.

The second is the rendering connection above. A policy interacts directly with whether scripts run, and whether scripts run decides what a crawler sees on a client-rendered site. That places it in the same territory as the rendering checks, from an unexpected direction.

The Important rating reflects both the genuine value and the genuine risk. There is no visibility consequence to missing this, which keeps it out of the top band. And the work is not one line: introducing a policy properly is a fortnight of reports and a set of decisions about your own tooling, which is a fair amount to ask and worth asking for.

Questions this check raises

What does a CSP actually prevent?
It stops the browser executing resources from sources you did not list, which contains the impact of an injection: the attacker gets markup onto the page and the script never runs. It also prevents data exfiltration to unlisted destinations and, with frame-ancestors, framing by other sites.
How long should I run report-only mode?
Long enough to cover a full cycle of your traffic, which usually means weeks rather than days. Third-party tags, A/B tools, payment iframes and marketing scripts appear on paths nobody tests, and each one that is missing from the policy is a feature that silently stops working when you enforce.
Why does a security header appear in an SEO audit?
Because these headers affect whether the page works, and a page whose scripts are blocked or whose assets are refused is a broken page regardless of how it ranks. They also form part of the trust signals a site is assessed on, which is the category this pillar covers.