Cloudflare Clickjacking Protection
Setting X-Frame-Options and CSP frame-ancestors on Cloudflare.
Which method should you use?
Cloudflare gives you three ways to add response headers. Pick based on your setup:
- Managed Transforms -- one click, no configuration. Adds
x-frame-options: SAMEORIGINplus four other security headers. Good if you want a quick baseline andSAMEORIGINfits your needs. Available on all plans. - Response Header Transform Rules -- the method you will use most often. Set any header with any value. Apply to all requests or filter by URL. Available on all plans (10 rules on Free, 25 on Pro).
- Pages
_headersfile -- for static sites deployed on Cloudflare Pages. Define headers per-route in a plain text file in your build output.
Method 1: Managed Transforms (one click)
Cloudflare ships a Managed Transform called Add security headers. Enable it and Cloudflare adds these headers to every response:
x-content-type-options: nosniff
x-xss-protection: 1; mode=block
x-frame-options: SAMEORIGIN
referrer-policy: same-origin
expect-ct: max-age=86400, enforceSteps:
- Open the Cloudflare dashboard and select your domain.
- Go to Security > Settings.
- Under Managed Transforms, toggle on Add security headers.
The header takes effect immediately. Run your site through ClickJack Test to confirm.
DENY. Use a Transform Rule instead (Method 2) for that.Also worth noting: the Managed Transform adds x-xss-protection, which sets a header that was removed from all modern browsers years ago. It does no harm, but you can see why a custom Transform Rule gives you tighter control.
Method 2: Response Header Transform Rules
This is the recommended approach. You set exactly the headers and values you need, and you can apply them to all requests or filter by URL pattern. Available on all plans.
Dashboard setup
- Open the Cloudflare dashboard and select your domain.
- Go to Rules > Transform Rules.
- Click Create rule > Response Header Transform Rule.
- Give the rule a descriptive name, e.g. “Clickjacking protection”.
- Under When incoming requests match, choose All incoming requests (or define a custom filter expression if you only want headers on certain paths).
- For Modify response header, select Set static. Enter:
- Header name:
Content-Security-Policy - Value:
frame-ancestors 'none'
- Header name:
- Click Set new header. Add a second header for X-Frame-Options. Select Set static. Enter:
- Header name:
X-Frame-Options - Value:
DENY
- Header name:
- Click Deploy.
One header is enough. Both gives defense in depth: CSP takes precedence in modern browsers, X-Frame-Options covers older ones. If you embed your own pages in iframes, use 'self' and SAMEORIGIN instead.
Transform Rules apply at the Cloudflare edge before the response reaches the browser. They do not modify the response your origin server sends -- they add to it. This means you can add clickjacking headers without touching your backend code, which is the whole point of using Cloudflare for this.
Transform Rules also apply to Cloudflare error pages and Custom Errors -- so even if your origin is down, Cloudflare still adds the header to the error page it serves.
If your origin already sets these headers
If your origin server already responds with X-Frame-Options or CSP, a Transform Rule using Set static will overwrite the origin's value. If you want to keep the origin's header and add your own, use Add static instead -- but this results in duplicate headers, which is sloppy. Better to pick one place (origin or Cloudflare) and stick with it.
API setup
If you manage Cloudflare via API, here is the equivalent rule. You need a token with the Transform Rules: Edit permission.
curl "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/rulesets/$RULESET_ID" \
--request PUT \
--header "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
--json '{
"rules": [
{
"description": "Clickjacking protection",
"expression": "true",
"action": "rewrite",
"action_parameters": {
"headers": {
"Content-Security-Policy": {
"operation": "set",
"value": "frame-ancestors '\''none'\''"
},
"X-Frame-Options": {
"operation": "set",
"value": "DENY"
}
}
}
}
]
}'If no ruleset exists yet for the http_response_headers_transform phase, create one first using the Create zone ruleset endpoint with kind: zone and phase: http_response_headers_transform. Check the Cloudflare API docs for full details.
Method 3: Cloudflare Pages _headers file
If you deploy on Cloudflare Pages (static sites), you can define headers in a _headers file in your build output directory. This file is not served to visitors -- Cloudflare parses it and applies the rules to your static asset responses.
The file belongs in the public/ or static/ directory (whichever your framework copies to the build output).
# public/_headers
/*
Content-Security-Policy: frame-ancestors 'none'
X-Frame-Options: DENYThe rules:
- First line is the URL pattern.
/*matches everything. You can use paths like/app/*for specific routes. - Following indented lines are
Name: Valueheader pairs. Indentation matters -- use spaces or tabs consistently. - Up to 100 header rules per file. Each line limited to 2,000 characters.
- If the same header is set by multiple matching rules, values are joined with a comma.
To remove a header set by a broader rule, prefix the header name with !:
/*
Content-Security-Policy: frame-ancestors 'none'
/embeddable-widget/*
! Content-Security-PolicyImportant: the _headers file only applies to static assets. If your Pages project uses Functions, those dynamic responses need headers set in the Function code itself.
What about Workers?
If you use Cloudflare Workers, set headers in the Worker response:
export default {
async fetch(request, env, ctx) {
const response = await fetch(request);
const newHeaders = new Headers(response.headers);
newHeaders.set("Content-Security-Policy", "frame-ancestors 'none'");
newHeaders.set("X-Frame-Options", "DENY");
return new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers: newHeaders,
});
},
};Workers is the most flexible option -- you can make header values dynamic based on request data -- but it is overkill for a static header. Transform Rules are simpler and do not consume Worker request quotas.
Verify it is working
After deploying your rule, check that the headers are present:
curl -I https://yoursite.comLook for X-Frame-Options: DENY or Content-Security-Policy: frame-ancestors 'none' in the output.
Also test a path that would normally return an error -- Transform Rules apply to Cloudflare error pages too:
curl -I https://yoursite.com/nonexistentThen run it through ClickJack Test to confirm.
Plan limits
| Plan | Active Transform Rules | Regex support |
|---|---|---|
| Free | 10 | No |
| Pro | 25 | No |
| Business | 50 | Yes |
| Enterprise | 300 | Yes |
Source: Cloudflare Transform Rules docs, retrieved June 2025.