Skip to content
ClickJack Test

← Guides

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:

  • Managed Transforms: one click, no configuration. Adds x-frame-options: SAMEORIGIN plus four other headers. Fine as a quick baseline if SAMEORIGIN suits you.
  • Response Header Transform Rules: the one to use. Set any header to any value, on every request or only matching URLs. Available on all plans (10 rules on Free, 25 on Pro).
  • Pages _headers file: for sites deployed on Cloudflare Pages. A plain text file in your build output that sets headers per path, for static assets only.
Skip Page Rules. Page Rules have no setting for arbitrary response headers, and Cloudflare treats them as legacy: it plans to migrate existing Page Rules to the newer Rules products automatically. Use a Transform Rule.

Method 1: Managed Transforms (one click)

The Add security headers Managed Transform 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, enforce

Steps:

  1. Open the Cloudflare dashboard and select your domain.
  2. Go to Rules > Settings and open the Managed Transforms tab.
  3. Toggle on Add security headers.

It takes effect within seconds. Run your site through ClickJack Test to confirm.

This sets SAMEORIGIN, not DENY. If you never frame your own pages, you want DENY, which needs a Transform Rule (Method 2).

It also sends x-xss-protection and expect-ct, both deprecated and ignored by current browsers. Harmless, but another reason to prefer a Transform Rule you control. It sends no Content-Security-Policy.

Method 2: Response Header Transform Rules

The recommended approach: you choose the exact headers and values, for all requests or only matching URLs. Available on all plans.

Dashboard setup

  1. Open the Cloudflare dashboard and select your domain.
  2. Go to Rules > Overview.
  3. Click Create rule > Response Header Transform Rule.
  4. Name it, e.g. “Clickjacking protection”.
  5. Under When incoming requests match, choose All incoming requests (or a custom filter expression to limit it to certain paths).
  6. For Modify response header, select Set static. Enter:
    • Header name: Content-Security-Policy
    • Value: frame-ancestors 'none'
  7. Click Set new header, select Set static again and enter:
    • Header name: X-Frame-Options
    • Value: DENY
  8. Click Deploy.

Either header alone protects modern browsers. Send both: when a CSP frame-ancestors directive is present, browsers ignore X-Frame-Options, so it only matters to older clients. If you frame your own pages, use 'self' and SAMEORIGIN instead.

The rule rewrites responses at Cloudflare's edge, so your origin code doesn't change. It also applies to Cloudflare's own error pages and Custom Errors, so the header is still sent when your origin is down.

If your origin already sets these headers

Set static replaces any header of the same name from your origin. That is what you want for X-Frame-Options, but for Content-Security-Policy it throws away the origin's whole policy, including script-src and every other directive. If your origin already sends a CSP, either add frame-ancestors 'none' to that policy at the origin, or use Add static for the CSP header. Browsers enforce every CSP header they receive, so a second header containing only frame-ancestors 'none' blocks framing even if the origin's policy allows it.

API setup

The same rule via the API. The token needs 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"
            }
          }
        }
      }
    ]
  }'

This PUT replaces every rule in the ruleset, so include any existing rules you want to keep. If no ruleset exists yet for the http_response_headers_transform phase, create one with the Create zone ruleset endpoint, using kind: zone and phase: http_response_headers_transform. See the Cloudflare API docs.

Method 3: Cloudflare Pages _headers file

On Cloudflare Pages you can set headers in a _headers file in your build output directory. Cloudflare doesn't serve the file; it applies its rules to your static asset responses.

Put it in public/ or static/, whichever your framework copies to the build output. Without a framework, put it in the build output directory itself.

# public/_headers
/*
  Content-Security-Policy: frame-ancestors 'none'
  X-Frame-Options: DENY

The rules:

  • The first line of each block is the URL pattern. /* matches everything; use paths like /app/* for specific routes.
  • Each header goes on an indented line below it as Name: Value.
  • Up to 100 header rules per file, and 2,000 characters per line.
  • If more than one matching rule sets the same header, the values are joined with a comma, so don't set a framing header in two overlapping rules.

To remove a header set by a broader rule, prefix its name with ! (an exclamation mark and a space):

/*
  Content-Security-Policy: frame-ancestors 'none'

/embeddable-widget/*
  ! Content-Security-Policy

Important: the _headers file only applies to static assets. Responses generated by Pages Functions (SSR, API routes) don't get these headers, even when the URL matches. Set them in the Function code, or cover the whole zone with a Transform Rule.

Next.js on Pages: next.config headers() isn't enough

We learned this one the hard way. ClickJack Test is a Next.js app built with @cloudflare/next-on-pages (now deprecated in favour of the OpenNext adapter on Workers). It set X-Frame-Options and frame-ancestors in next.config headers(), and our own tester still rated the homepage vulnerable. The next-on-pages router discards headers collected from headers() when it reaches the route Next.js generates for middleware, and this site has a middleware file.

We now set the headers in two places. A /* rule in public/_headers covers responses served from static assets:

# public/_headers
/*
  X-Frame-Options: DENY
  Content-Security-Policy: frame-ancestors 'none'

_headers doesn't reach responses the Function renders, such as 404 pages and API routes, so middleware sets the same headers on everything it returns (next-on-pages applies middleware headers after the reset):

// src/middleware.ts
import { NextResponse } from 'next/server';

export function middleware() {
  const res = NextResponse.next();
  res.headers.set('X-Frame-Options', 'DENY');
  res.headers.set('Content-Security-Policy', "frame-ancestors 'none'");
  return res;
}

After you deploy, curl a static page, a URL that 404s, and an API route. All three should carry both headers.

What about Workers?

If a Worker sits on a route in front of your origin, set the headers on the response it returns:

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,
    });
  },
};

A Worker lets you vary the value per request, but for a fixed header it is overkill: a Transform Rule is simpler and doesn't count against Worker request limits. Workers with Static Assets also support a _headers file, with the same rule as Pages: it covers static assets, not responses your Worker code generates.

Verify it is working

After deploying, check the headers:

curl -I https://yoursite.com

Look for X-Frame-Options: DENY and Content-Security-Policy: frame-ancestors 'none' in the output.

Also check a path that returns an error. Transform Rules cover error responses too; a Pages _headers file may not:

curl -I https://yoursite.com/nonexistent

Then run it through ClickJack Test to confirm.

Plan limits

PlanActive Transform RulesRegex support
Free10No
Pro25No
Business50Yes
Enterprise300Yes

Source: Cloudflare Transform Rules docs, checked October 2026.

Check your headers

See whether a site sends X-Frame-Options or CSP frame-ancestors, and what browsers will do with them.