In a clickjacking attack, another website loads your page inside an invisible frame and tricks visitors into clicking buttons on it, such as "Delete", "Buy" or "Allow", while they think they are clicking something else.
You stop it by telling browsers that your pages may not be shown inside a frame on another site. This guide shows how to do that with two HTTP headers.
Quick answer
Add these two headers to every page your site sends. On Apache or LiteSpeed (most cPanel hosting), put this in the .htaccess file in your site's root folder:
<IfModule mod_headers.c>
Header always set Content-Security-Policy "frame-ancestors 'self'"
Header always set X-Frame-Options "SAMEORIGIN"
</IfModule>
This lets your own site frame its pages and blocks every other site. frame-ancestors is the modern header; X-Frame-Options covers older browsers. MDN
On Cloudflare, you can add the same two headers with a Response Header Transform Rule instead (see below).
What you need: access to your site's files (cPanel File Manager, FTP or SSH), or to the web server configuration.
Which value to use
| You want | Content-Security-Policy | X-Frame-Options |
|---|---|---|
| No site may frame your pages, not even yours | frame-ancestors 'none' |
DENY |
| Only your own site may frame them | frame-ancestors 'self' |
SAMEORIGIN |
| Your site plus one partner site | frame-ancestors 'self' https://partner.example |
not possible: send only the CSP header |
SAMEORIGIN is the safe choice for most sites, including WordPress, because some admin screens and page builders load your own pages in a frame.
Do not use ALLOW-FROM in X-Frame-Options. It is obsolete, and modern browsers ignore the whole header when they see it. MDN
Step 1: Add the headers
Apache or LiteSpeed (.htaccess)
- In cPanel, open File Manager and go to your site's root folder (usually
public_html). - Click Settings and tick Show Hidden Files (dotfiles), so
.htaccessis visible. - Right-click
.htaccessand choose Edit. If there is no.htaccess, create it. - Add the block from the Quick answer at the top of the file. On WordPress, put it above the
# BEGIN WordPressline, because WordPress rewrites everything between# BEGIN WordPressand# END WordPress. - Save the file.
The <IfModule> lines stop the site from breaking with an error 500 if the mod_headers module is missing. If the headers do not appear in Step 2, ask your host to enable mod_headers.
Nginx
Add these lines to the server block of your site, then reload Nginx:
add_header Content-Security-Policy "frame-ancestors 'self'" always;
add_header X-Frame-Options "SAMEORIGIN" always;
sudo nginx -t && sudo systemctl reload nginx
Nginx only passes add_header lines down to a location block that has no add_header lines of its own. If a location block already sets a header, repeat these two lines inside it.
WordPress without .htaccess access
If you cannot edit .htaccess (for example on Nginx hosting without server access), WordPress can send the headers itself. Add this to your theme's functions.php, or better, to a small plugin, so a theme update does not remove it:
add_action('send_headers', function () {
header("Content-Security-Policy: frame-ancestors 'self'");
header('X-Frame-Options: SAMEORIGIN');
});
This only covers pages WordPress builds. Static files such as images and PDFs are not protected, which is usually fine, because those are not targets for clickjacking.
Cloudflare (no server access needed)
If your domain uses Cloudflare, Cloudflare can add the headers for you. This works whatever your web server is, and it also covers images, PDFs and other static files. Cloudflare
- Log in to the Cloudflare dashboard and choose your domain.
- Go to Rules › Overview.
- Click Create rule › Response Header Transform Rule.
- Under Rule name, enter
Clickjacking protection. - Under When incoming requests match, choose All incoming requests.
- Under Modify response header, choose Set static, then enter:
- Header name:
Content-Security-Policy - Value:
frame-ancestors 'self'
- Header name:
- Click Set new header and add a second one, again with Set static:
- Header name:
X-Frame-Options - Value:
SAMEORIGIN
- Header name:
- Click Deploy.
Set static replaces any header with the same name that your server already sends, so you do not end up with two copies. Add static would keep both. Cloudflare
The rule only works when the domain's DNS records are Proxied (orange cloud) in DNS › Records. Records set to DNS only (grey cloud) go straight to your server and do not get the headers.
If your site already sends a Content-Security-Policy with other rules in it, do not use Set static for Content-Security-Policy, because it would replace your whole policy. Add frame-ancestors to the policy on your server instead, and use Cloudflare only for X-Frame-Options.
Headers in a meta tag do not work
Neither header works in an HTML <meta http-equiv> tag. Browsers ignore both there; they must be sent as real HTTP headers. MDN MDN
Step 2: Check it worked
From a terminal, replace the address with your site:
curl -sI https://www.example.com/ | grep -iE 'content-security-policy|x-frame-options'
You should see both lines:
content-security-policy: frame-ancestors 'self'
x-frame-options: SAMEORIGIN
Without a terminal: open your site in Chrome or Firefox, press F12, open the Network tab, reload, click the first request and look under Response Headers.
Troubleshooting
Error 500 after editing .htaccess. There is a typo in the file. Remove the lines you added, save, and add them again carefully. Keep the <IfModule mod_headers.c> wrapper.
A header appears twice, with different values. Another layer already sends it: a security plugin, the host's server configuration, or a CDN such as Cloudflare. Keep it in one place only. With two different X-Frame-Options values, browsers may ignore the header.
Your site already sends a Content-Security-Policy header. Do not add a second one. Add frame-ancestors 'self' to the existing policy, separated from the other directives with a semicolon. frame-ancestors is not covered by default-src, so it must be listed on its own. MDN
A legitimate site can no longer embed your pages (for example a booking widget on a partner site). Add that site to frame-ancestors, as in the table above, and remove X-Frame-Options, which cannot allow a specific other site.
WordPress Customizer or a page builder stops showing a preview. You used 'none' / DENY. Change to 'self' / SAMEORIGIN.