Skip to content
ClickJack Test

← Guides

WordPress Clickjacking Protection

Setting X-Frame-Options and CSP frame-ancestors on a WordPress site.

Which method should you use?

WordPress core already protects its own admin screens: since WordPress 6.9, wp-admin and wp-login.php send X-Frame-Options: SAMEORIGIN and frame-ancestors 'self' (older versions sent only X-Frame-Options). Your public pages get nothing by default. There are four ways to fix that. Pick one you can access:

  • wp-config.php: two lines, no theme or plugin needed. Runs on every request that loads WordPress, but not on pages your web server serves from a static cache.
  • Theme functions.php: uses the core wp_headers filter. Covers front-end pages only. Put it in a child theme so updates do not overwrite it.
  • .htaccess (Apache) / server config: the web server adds the headers to every response, whether or not WordPress runs. The most reliable method if your host runs Apache and allows .htaccess overrides.
  • Security plugin: no code needed, but few plugins offer this for free.
Server-level beats PHP-level. When possible, add headers in .htaccess or your web server config. Headers set at the server level apply to every response, including cached pages, error pages, and static files that bypass WordPress entirely. Use one method, not several: two sources sending different values can block framing you meant to allow.

Any of these headers also stops other sites from showing your posts as WordPress embeds, since those load your /embed/ URLs in an iframe.

Method 1: wp-config.php (two lines)

Add this line to your wp-config.php file, just before the "That's all, stop editing!" comment:

// Clickjacking protection
header('X-Frame-Options: DENY');
header("Content-Security-Policy: frame-ancestors 'none'");

/* That's all, stop editing! Happy publishing. */

Use SAMEORIGIN and 'self' if you embed your own pages in iframes.

These calls run on every request that loads WordPress, including the REST API. On wp-admin, the login page and the Customizer preview, core sends its own SAMEORIGIN and 'self' headers later in the request, which replace yours, so admin screens keep working.

Steps:

  1. Connect to your site via FTP, cPanel File Manager, or SSH.
  2. Open wp-config.php in the WordPress root directory.
  3. Add the lines above before the final comment block.
  4. Save and upload.

Run your site through ClickJack Test to confirm the headers are present.

Caching caveat. wp-config.php runs before a cache plugin's advanced-cache.php, so pages the plugin serves through PHP keep the headers. Pages the web server serves straight from disk via rewrite rules (WP Super Cache Expert mode, W3 Total Cache Disk: Enhanced, WP Rocket on Apache) never touch PHP and will not have them. Use .htaccess or server config for those. Server caches and CDNs usually store headers with the page, so purge them after adding headers.

Method 2: Theme functions.php (via wp_headers filter)

WordPress has a built-in filter for modifying response headers. Add this to your theme's functions.php file:

/**
 * Add clickjacking protection headers.
 * Use a child theme so this survives parent theme updates.
 */
function cjt_add_security_headers($headers) {
    $headers['X-Frame-Options'] = 'DENY';
    $headers['Content-Security-Policy'] = "frame-ancestors 'none'";
    return $headers;
}
add_filter('wp_headers', 'cjt_add_security_headers');

Paste it into your child theme's functions.php so a parent theme update does not wipe it. A small plugin in wp-content/mu-plugins/ works too and survives theme switches.

WordPress applies this filter in WP::send_headers(), which runs for front-end requests: pages, posts, archives and feeds. It does not run for wp-admin, the login page or the REST API. Core already covers the first two. Keep the default priority (10): the Customizer adds its own wp_headers filter later at the same priority to allow its same-origin preview iframe, and a higher priority on yours would break that preview.

Caching hits this method harder than wp-config.php. On a page cache hit, the cache plugin answers before WP::send_headers() runs, so the headers are missing unless the plugin stores and replays response headers. Test a cached page before relying on it.

Method 3: .htaccess (Apache)

If your host runs Apache and allows .htaccess overrides, this is the most reliable method. Apache adds the headers to every response it sends, so they cover statically cached pages, error pages and static files.

Add this to the .htaccess file in your WordPress root directory:

# Clickjacking protection -- place above "# BEGIN WordPress"
<IfModule mod_headers.c>
    Header always set Content-Security-Policy "frame-ancestors 'self'"
    Header always set X-Frame-Options "SAMEORIGIN"
</IfModule>

This uses 'self' and SAMEORIGIN on purpose. Apache applies it to wp-admin too, overriding core's headers, and WordPress frames its own pages: the Customizer previews your site in an iframe, and the plugin "View details" dialog is one. 'none' here would break both. Same-origin still blocks every other site from framing you.

The IfModule block prevents a 500 error if mod_headers is not loaded, but then no headers are sent, so check the result. The always keyword also covers responses Apache generates itself, such as redirects and error pages, and lets the directive replace headers PHP sends through PHP-FPM.

Put these lines above the # BEGIN WordPress block. WordPress rewrites everything between # BEGIN WordPress and # END WordPress when it regenerates its rules, such as when you save permalink settings.

Method 4: Security plugins

Useful if you want a UI and do not want to touch code. Plugins set headers from PHP, so the caching caveats above still apply. The Sucuri firewall is the exception: it adds headers at its proxy.

Shield Security (free)

Go to Configure > HTTP Headers > Basic HTTP Headers and set Block iFrames to "Allow iFrames on the same domain" (SAMEORIGIN) or "Block all iFrames" (DENY). This option is in the free plugin. It controls X-Frame-Options only; custom CSP rules, where frame-ancestors would go, need ShieldPRO.

Really Simple Security (Pro)

Formerly Really Simple SSL. Security headers are a Pro feature; the free plugin does not send them. In Pro, go to Security > Settings > Security Headers to set X-Frame-Options (SAMEORIGIN or DENY). Pro keeps it in sync with the frame-ancestors directive of the Content-Security-Policy it can generate.

Sucuri

The Sucuri Website Firewall (a paid WAF) adds X-Frame-Options, X-XSS-Protection and X-Content-Type-Options through its Additional Security Headers option on the Security tab of the firewall settings. Sucuri says it is on by default. It does not add CSP frame-ancestors.

The free Sucuri Security plugin has a Headers Management page, but its CSP option only sends Content-Security-Policy-Report-Only, which reports violations without blocking anything, and it has no X-Frame-Options setting. It does not protect you from clickjacking.

Wordfence does not set security headers. Wordfence is a firewall and malware scanner. It does not add X-Frame-Options, CSP, or other HTTP security headers. If you rely on Wordfence alone, only wp-admin and the login page are protected (by core). Add headers using one of the methods above.

What about NGINX?

If your WordPress host runs NGINX, you cannot use .htaccess. Add headers in your NGINX server block instead. See the NGINX guide for the full walkthrough.

Managed WordPress hosts such as Kinsta, WP Engine and Pressable serve traffic through NGINX, ignore .htaccess, and usually do not give you the server config. WP Engine lets you add response headers yourself under Web Rules > Header Rules in its User Portal. Elsewhere, ask support to add the headers, or use the wp-config.php or functions.php method.

A note on the meta tag that does not work

You may see advice to add this to your WordPress header:

<meta http-equiv="X-Frame-Options" content="DENY">

This does nothing. Browsers ignore X-Frame-Options in a meta tag, and they ignore frame-ancestors in a CSP meta tag too. Only an HTTP response header works. If a security scanner recommended this, the scanner is wrong.

Verify it is working

After adding the headers, check that they appear:

curl -I https://yoursite.com

Look for a Content-Security-Policy header with frame-ancestors 'none' or 'self', and X-Frame-Options set to DENY or SAMEORIGIN. Each should appear once.

If you use page caching, run the command twice: curl is logged out, so the second request is normally served from the cache. If the headers are missing there, the PHP methods (wp-config.php, functions.php, plugins) are not enough. Switch to .htaccess or server config.

Then run it through ClickJack Test to confirm.

Check your headers

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