Content Security Policy on Cloudflare
Cloudflare gives you three different ways to set a CSP header — Transform Rules, Workers and the legacy Page Rules — and each has different limits, costs and edge cases. This guide walks through all three, then covers the Cloudflare-specific pitfalls (Email Obfuscation, Rocket Loader, header collisions with the origin) that break a CSP after you deploy it.
Option 1: Transform Rules (recommended for most sites)
Transform Rules are Cloudflare's modern, zero-code way to add or rewrite headers at the edge. They're available on every plan including Free (with quota limits), and they're the right default for almost every CSP rollout.
In the Cloudflare dashboard, select your zone and navigate to:
Rules → Transform Rules → Modify Response Header → Create rule
- Rule name:
Set CSP header - When incoming requests match:
All incoming requests(or scope it by hostname/URI path) - Then… Click Set static and fill in:
- Header name:
Content-Security-Policy-Report-Only - Value:
default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'
- Header name:
Save and deploy. Cloudflare propagates the rule globally in a few seconds. Verify with curl -I https://your-site.example — the header should show up exactly once.
Set vs. Add — pick the right action
Transform Rules give you two action types and getting them wrong is the most common Cloudflare CSP bug:
- Set static — replaces any existing header with the same name. Use this when you want Cloudflare to be the single source of truth for the CSP, regardless of what the origin sends.
- Add static — appends a new header next to whatever the origin already sent. If the origin also sends a CSP, you end up with two CSP headers, which browsers treat as the intersection (more restrictive than either policy alone). This silently breaks things you didn't expect.
Rule of thumb: if your origin sends no CSP at all, either action works. If the origin does send a CSP, almost always use Set.
Free plan quota
The Free plan caps Transform Rules at 10 rules per zone across all rule types (URL Rewrite, Modify Request Header, Modify Response Header). One CSP rule is one slot. If you're already near the limit, either upgrade to Pro (50 rules) or fall back to a Worker.
Option 2: Cloudflare Workers (when Transform Rules aren't enough)
A Worker gives you full programmatic control over the response, which is overkill for a static CSP but useful if you need to:
- Rotate a per-request
noncefor inline scripts (the strict-CSP pattern) - Apply different policies based on the path, cookie, or user-agent
- Inject a
report-uriwith a request-specific token
Minimal example:
export default {
async fetch(request, env, ctx) {
const response = await fetch(request);
const headers = new Headers(response.headers);
headers.set(
'Content-Security-Policy-Report-Only',
"default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'"
);
return new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers,
});
},
};
Deploy with npx wrangler deploy and route it to your zone. Workers cost $5/month for the Paid plan with 10M requests included; the Free plan covers 100k requests/day, which is enough for small sites but not for production traffic.
Option 3: Page Rules (legacy — avoid for new setups)
Older Cloudflare guides reference Page Rules for header manipulation, but Page Rules cannot set arbitrary response headers — only specific feature toggles like cache TTL and SSL mode. Some tutorials suggest combining Page Rules with Workers to set headers, which works but is needlessly complicated. Cloudflare itself is in the process of migrating Page Rules to Rules-engine equivalents, so for any new CSP rollout use Transform Rules directly.
Cloudflare-specific pitfalls that break CSP
Email Obfuscation injects an inline script
Cloudflare's Email Address Obfuscation feature (Scrape Shield → Email Address Obfuscation) automatically rewrites mailto: links and email text on every HTML response to defeat scrapers. The way it does this is by injecting an inline <script> block plus a tiny external loader — so the moment you deploy a strict CSP without 'unsafe-inline' in script-src, the obfuscation breaks and the browser logs a CSP violation for every page load.
Two options:
- Disable Email Obfuscation per zone in Scrape Shield. If you don't have
mailto:links on the page, the feature does nothing useful anyway. - Allow the Cloudflare email decode origin in your policy:
script-src 'self' https://ajax.cloudflare.com. The inline part still needs'unsafe-inline'or a nonce, which is why disabling the feature is usually cleaner.
Rocket Loader rewrites every script tag
Rocket Loader (Speed → Optimization) intercepts every <script> tag, defers it, and re-injects the code through Cloudflare's own loader. The injected loader runs from https://ajax.cloudflare.com as inline JavaScript with a Cloudflare-controlled hash. With strict CSP this is a non-starter — Rocket Loader fundamentally cannot work without 'unsafe-inline' in script-src.
If you want a strict CSP, disable Rocket Loader. If you want Rocket Loader, accept that you'll need 'unsafe-inline' https://ajax.cloudflare.com in script-src, which substantially weakens the policy.
Origin sends its own CSP and Cloudflare appends a second one
If your origin (Apache, Nginx, WordPress, etc.) already sends a CSP and your Transform Rule uses Add instead of Set, the browser receives two Content-Security-Policy headers and applies their intersection. Common symptom: things that worked yesterday suddenly get blocked, even though you only "added an exception". Verify with:
curl -I https://your-site.example | grep -i content-security-policy
If you see the header twice, change the Transform Rule action from Add to Set, or strip the origin's CSP at the origin.
Caching: edge-cached responses don't pick up new rules instantly
Transform Rules apply on every response, including ones served from Cloudflare's cache. But if you have aggressive cache settings on the origin (e.g. Cache-Control: public, max-age=86400) and the origin sends its own CSP header in those cached responses, the Transform Rule's Set action still runs at the edge — but you might see old caches in DevTools because of browser-side caching. To force a clean test, do Caching → Configuration → Purge Everything after rule changes and test from an incognito window.
HTTP/3 and header order
Cloudflare prefers HTTP/3 (QUIC) for browser traffic. HTTP/3 normalises header names to lowercase and may reorder them on the wire. This is harmless for CSP — browsers don't care about case or order — but it can confuse debugging tools that show headers verbatim. Use curl --http2 -I to get a stable view if HTTP/3 output is misleading.
Recommended rollout order
- Generate your CSP. Either start from a generic template and watch the violations roll in, or skip the trial and error and let our scanner build a tailored policy from your actual loaded resources.
- Disable Email Obfuscation and Rocket Loader for the zone (or accept they need
'unsafe-inline'). - Create the Transform Rule with
Content-Security-Policy-Report-Onlyand the Set static action. - Monitor the browser console and your
report-uriendpoint (if you set one) for 1–2 weeks. - Switch the header name from
Content-Security-Policy-Report-OnlytoContent-Security-Policyin the same rule. - Purge cache and verify on production from a clean browser session.