Multiple CSP policies on one page
CentralCSP Team ·
Last update:
You can send more than one Content Security Policy (CSP) to a single page, and the browser will honor all of them at once. A CSP is an HTTP response header that tells the browser which scripts, styles, and other resources a page may load. When a page carries several of these, the browser does not merge them into one policy. It checks each one on its own, and a resource has to pass every one of them to load.
That single rule explains almost everything about multiple policies: adding a policy can only make a page stricter, never looser. A second header cannot re-allow something the first one blocked. This post shows how the policies combine, the gotchas that surprise people, and how to test a tighter policy safely before you enforce it.
How a browser combines multiple policies
A page can pick up policies from several places at the same time:
- Multiple
Content-Security-PolicyHTTP response headers. - A single
Content-Security-Policyheader that contains several policies separated by commas. Each comma-separated segment counts as its own policy. - A
<meta http-equiv="Content-Security-Policy">element, enforced alongside the headers. - One or more
Content-Security-Policy-Report-Onlyheaders, which report violations but never block.
The browser collects every active policy into one list and evaluates them independently. For a given request it starts from "allowed", then walks the list. If any enforced policy is violated, the request is blocked, and nothing later in the list can flip it back to allowed. That is the mechanism behind the phrase you will hear often, "the most restrictive policy wins".
Report-Only policies are skipped during this blocking decision. They are evaluated only to produce reports, so they shape what you see in your monitoring, not what the browser actually loads.
A resource must satisfy every policy
Here is the canonical example, two Content-Security-Policy headers on the same response:
Content-Security-Policy: default-src 'self' http://example.com;
connect-src 'none';Content-Security-Policy: connect-src http://example.com/;
script-src http://example.com/The second header lists connect-src http://example.com/, so on its own it would allow that connection. It does not matter. The first policy sets connect-src 'none', and the request has to pass both. Because one policy forbids all connections, the connection is blocked, and the effective rule for connect-src is 'none'.
MDN describes the combination the same way: adding policies "can only further restrict the capabilities of the protected resource". There is no way to write a second policy that loosens the first.
The order of the headers does not change the outcome. Whether connect-src 'none' arrives first or second, the result is identical, because every policy is checked independently and the request must clear all of them.
A request loads only when it clears every enforced policy, so the effective rule is the intersection, the most restrictive wins:
The intersection gotcha with host allowlists
The "must pass every policy" rule has a sharp edge when two policies allow overlapping but different sets of hosts. People expect the browser to take the union of the hosts. It does the opposite.
Content-Security-Policy: script-src 'self' https://trusted.com https://analytics.comContent-Security-Policy: script-src 'self' https://trusted.comA script from https://analytics.com passes the first policy but fails the second, so it is blocked. A script from https://trusted.com passes both, so it loads. The effective result is the intersection of the two allowlists, 'self' and https://trusted.com, because a resource has to satisfy each policy on its own.
This is easy to cause by accident. If one team adds a CSP at the CDN or proxy and another adds one in the app, a host that only one of them allows is effectively blocked. When a resource is allowed by one policy but not the other, it does not load, full stop. If you are debugging a resource that "should" be allowed, check whether a second policy is in play before you touch the directive you were looking at.
default-src fallback is resolved inside each policy separately, before the cross-policy check applies. The browser never builds one merged policy, so a directive's fallback in policy A has no effect on policy B.
For a single page that needs a clear, maintainable policy, prefer consolidating into one well-structured header rather than spreading directives across several. Our CSP evaluator flags weak or contradictory directives so you can see the real effective policy before you ship it.
Test a stricter policy without breaking the page
The most useful reason to run more than one policy is to enforce what works today while trialing something tighter. You enforce one policy and run the stricter one in Report-Only at the same time:
Content-Security-Policy: default-src 'self'; script-src 'self' https://trusted.comContent-Security-Policy-Report-Only: default-src 'self'; script-src 'self'Scripts from https://trusted.com still load, because the enforced policy allows them. The Report-Only policy is stricter, so the browser also sends a violation report for each of those scripts, without blocking anything. You read those reports, confirm the tighter policy would not break the page, then promote it to the enforced header.
To collect the reports, set up the browser Reporting API and point the policy at an endpoint. Send a Reporting-Endpoints header and reference it from the report-to directive in each policy:
Reporting-Endpoints: csp-endpoint="https://<Endpoint-ID>.report.centralcsp.com"Content-Security-Policy: default-src 'self'; script-src 'self' https://trusted.com; report-to csp-endpointContent-Security-Policy-Report-Only: default-src 'self'; script-src 'self'; report-to csp-endpointOne thing to plan for: each enforced policy that a request violates emits its own report. If you run several policies and a single blocked resource violates two of them, you get two reports for that one block. That is expected, since the browser reports per policy, but it means your raw report volume scales with the number of policies, not the number of distinct problems. Grouping reports by the resource and directive, rather than counting raw events, keeps the noise down. That is the kind of correlation CentralCSP does for you so the Report-Only trial reads as a short list of changes to make, not a flood of duplicate events.
What to remember
- Multiple policies are enforced independently. A resource has to pass all of them to load.
- An extra policy can only add restrictions. It can never re-allow what another policy blocks.
- Overlapping host allowlists intersect. A host allowed by only one policy is still blocked.
- Header order does not matter.
- Report-Only never blocks, so it is the safe way to trial a tighter policy next to a working one.
- A single blocked resource can produce one report per violated policy.
This behavior is part of CSP Level 3 and is supported across browsers that support CSP. If you want a head start, you can scan your current setup with the free CSP scanner and see how your policies combine before you change anything.
For more on the building blocks behind these examples, see how to build a strong CSP and getting started with CSP reporting.