Type something to search...
What is clickjacking and how to prevent it?

What is clickjacking and how to prevent it?

Clickjacking is an attack where a malicious website loads your site inside an invisible or disguised frame and tricks visitors into clicking buttons or links on your page without realizing it, such as confirming a purchase, changing a setting, or granting a permission. You prevent it by telling browsers not to let other sites frame your pages, using the Content-Security-Policy: frame-ancestors directive and the older X-Frame-Options header.

The name combines "click" and "hijacking," and the attack is also known as a UI redress attack. It's simple, effective against unprotected sites, and fortunately very easy to prevent. This article explains how clickjacking works, what attackers can do with it, and how to add the right protections on Apache, Nginx, WordPress, Next.js, Express, and Cloudflare, plus how to test that they're working.

How Does Clickjacking Work?

Browsers allow one web page to embed another using an iframe element. That's how embedded videos, maps, and payment widgets work. Clickjacking abuses this feature.

Here's the typical sequence:

  1. The attacker builds a decoy page: It shows something enticing, such as a "Click to claim your prize" button, a game, or a video play button.

  2. Your site is loaded in a hidden frame: The attacker embeds a page from your site, for example an account settings page, in an iframe and makes it fully transparent with CSS, positioning it precisely over the decoy.

  3. The victim is logged in to your site: Because browsers send cookies with framed requests (depending on cookie settings), the framed page shows the victim's authenticated view.

  4. The victim clicks: They think they're clicking the decoy button, but they're actually clicking a real button on your page underneath, such as "Delete account," "Make profile public," or "Confirm."

A simplified illustration of the technique looks like this:

<style>
  #target {
    position: absolute;
    top: 120px;
    left: 80px;
    width: 500px;
    height: 300px;
    opacity: 0;
    z-index: 2;
  }
  #decoy {
    position: absolute;
    top: 300px;
    left: 250px;
    z-index: 1;
  }
</style>

<button id="decoy">Click to play</button>
<iframe id="target" src="https://example.com/account/settings"></iframe>

The user sees only the "Click to play" button. The real click lands on whatever element of the framed page sits at that spot.

Variations of Clickjacking

Clickjacking comes in several forms:

  • Classic clickjacking: Tricking a user into clicking a single button on a hidden page.
  • Likejacking: Tricking users into liking or sharing social media content.
  • Cursorjacking: Displaying a fake cursor offset from the real one, so the user clicks somewhere other than where they think.
  • Multi-step clickjacking: Guiding users through several clicks, for example a fake game, to complete a multi-step action on the framed site.
  • Drag-and-drop and keystroke hijacking: Tricking users into dragging content or typing into hidden fields.
  • Permission hijacking: Tricking users into granting access to their camera, microphone, or notifications.

What Can Attackers Achieve?

The damage depends on what actions a single click (or a few clicks) can perform on your site:

  • Changing account or privacy settings.
  • Deleting content or accounts.
  • Making purchases or donations with one-click checkout.
  • Following, liking, or sharing content.
  • Approving OAuth permissions to third-party apps.
  • In an admin dashboard, activating a plugin or approving a user.

Any sensitive action that can be completed by clicking, without requiring typed secrets, is a potential target.

How to Prevent Clickjacking

The main defense happens in the browser: you send a response header that tells it which sites, if any, may frame your pages. If the header forbids framing, the browser refuses to render your page inside the attacker's iframe.

Option 1: Content-Security-Policy frame-ancestors (Recommended)

The modern standard is the CSP frame-ancestors directive. It's flexible and supported by all current browsers.

  • Block all framing: Content-Security-Policy: frame-ancestors 'none'
  • Allow only your own site: Content-Security-Policy: frame-ancestors 'self'
  • Allow specific trusted sites: Content-Security-Policy: frame-ancestors 'self' https://partner.example.net

frame-ancestors must be sent as an HTTP header. It is ignored if you put it in a meta tag.

Option 2: X-Frame-Options (Legacy)

X-Frame-Options is the older header, still widely used for compatibility with legacy browsers:

  • X-Frame-Options: DENY blocks all framing.
  • X-Frame-Options: SAMEORIGIN allows only your own origin to frame the page.

The old ALLOW-FROM value is obsolete and not supported by modern browsers, so use frame-ancestors if you need to allow specific sites. When both headers are present, modern browsers prioritize frame-ancestors. Sending both is a sensible belt-and-braces approach.

Which Value Should You Use?

  • If your site never needs to be framed, use frame-ancestors 'none' and X-Frame-Options: DENY.
  • If your own pages frame each other, which WordPress does in the Customizer and some admin features, use 'self' and SAMEORIGIN.

For most WordPress sites, SAMEORIGIN is the safer default because it avoids breaking the Customizer preview and similar features.

Adding Clickjacking Protection to Your Server

Always back up configuration files before editing them, and test your site afterwards.

Apache (.htaccess or Virtual Host)

This requires mod_headers, which is enabled on most hosts:

<IfModule mod_headers.c>
    Header always set X-Frame-Options "SAMEORIGIN"
    Header always set Content-Security-Policy "frame-ancestors 'self'"
</IfModule>

If you already send a Content Security Policy, add frame-ancestors 'self' to your existing policy rather than setting a second Content-Security-Policy header, since multiple policies combine in ways that can be confusing.

Nginx

Add these to your server block, then test and reload:

add_header X-Frame-Options "SAMEORIGIN" always;
add_header Content-Security-Policy "frame-ancestors 'self'" always;
sudo nginx -t && sudo systemctl reload nginx

Remember that in Nginx, add_header directives in a location block replace, rather than add to, those defined at the server level. If a location has its own add_header lines, repeat the security headers there.

WordPress (PHP)

WordPress already sends X-Frame-Options: SAMEORIGIN on the login page and admin screens using the send_frame_options_header() function. To protect the front end too, you can add headers from a small custom plugin or your child theme's functions.php:

add_action( 'send_headers', 'sajjad_send_anti_clickjacking_headers' );

function sajjad_send_anti_clickjacking_headers() {
    if ( headers_sent() ) {
        return;
    }
    header( 'X-Frame-Options: SAMEORIGIN' );
    header( "Content-Security-Policy: frame-ancestors 'self'" );
}

Setting headers at the server level is generally more reliable, because it also covers static files and cached pages served without running PHP. Some security plugins, like Headers Security Advanced & HSTS WP, can add these headers for you without code.

Next.js

In a Next.js project, add headers in next.config.js:

/** @type {import('next').NextConfig} */
const nextConfig = {
  async headers() {
    return [
      {
        source: "/:path*",
        headers: [
          { key: "X-Frame-Options", value: "SAMEORIGIN" },
          { key: "Content-Security-Policy", value: "frame-ancestors 'self'" },
        ],
      },
    ];
  },
};

module.exports = nextConfig;

If you already use a fuller CSP, include frame-ancestors in that policy instead. A separate article on this blog covers setting up a complete Content Security Policy in Next.js.

Node.js with Express

The Helmet middleware sets sensible security headers, including frame-ancestors and X-Frame-Options:

import express from "express";
import helmet from "helmet";

const app = express();

app.use(
  helmet({
    contentSecurityPolicy: {
      directives: {
        frameAncestors: ["'self'"],
      },
    },
    frameguard: { action: "sameorigin" },
  }),
);

Cloudflare

If your site is behind Cloudflare, you can add response headers at the edge using a response header transform rule (in the dashboard under Rules). This is handy when you can't easily change server configuration. Menu locations change over time, so check Cloudflare's documentation for the current path.

Additional Defenses

Headers are the primary fix, but a few extra measures help:

  • SameSite cookies: Cookies set with SameSite=Lax or Strict aren't sent to your site when it's loaded in a cross-site iframe, so the framed page appears logged out. WordPress core cookies don't set SameSite explicitly, but Chromium-based browsers treat cookies without it as Lax by default, so don't rely on this alone.
  • Confirm sensitive actions: Require re-entering a password or a 2FA code for critical actions like deleting accounts or changing email addresses.
  • Avoid one-click destructive actions: Add a confirmation step that requires typing or a deliberate second interaction.

What About Frame-Busting JavaScript?

Older sites used JavaScript snippets that checked whether the page was framed and tried to break out. These are unreliable, can be bypassed using iframe sandbox attributes, and aren't needed in modern browsers. Use headers instead.

How to Test Your Clickjacking Protection

Check Your Headers

From the command line:

curl -sI https://example.com | grep -iE 'x-frame-options|content-security-policy'

You should see your X-Frame-Options value and a CSP that includes frame-ancestors. You can also open your browser's developer tools, reload the page, and inspect the response headers in the Network tab.

Try Framing Your Site

Create a local test HTML file on your computer:

<!doctype html>
<html>
  <body>
    <h1>Framing test</h1>
    <iframe src="https://example.com" width="800" height="600"></iframe>
  </body>
</html>

Open it in a browser. If protection is working, the iframe will be blank or show an error, and the developer console will report that the page refused to be framed.

Online tools like Security Headers (securityheaders.com) and Mozilla Observatory also check for these headers.

Pages That Legitimately Need Framing

Sometimes you want your content embedded elsewhere, such as a booking widget on a partner's site. In that case:

  1. Scope the allowance: Apply a permissive frame-ancestors only to the specific paths that need embedding, not the whole site.
  2. List exact origins: Allow specific trusted domains rather than wildcards.
  3. Keep sensitive pages protected: Login, account, checkout, and admin pages should never be frameable by other sites.

In Nginx, for example, you can set a different policy for one path:

location /embed/ {
    add_header Content-Security-Policy "frame-ancestors 'self' https://partner.example.net" always;
    try_files $uri $uri/ /index.php?$args;
}

FAQ: Clickjacking

Clickjacking is when a malicious site hides your website in an invisible frame and tricks visitors into clicking buttons on your site while they think they're clicking something harmless on the attacker's page.

Use frame-ancestors in your Content Security Policy as the modern standard, and add X-Frame-Options for compatibility with older browsers. Modern browsers give frame-ancestors priority when both are present.

Partly. WordPress sends X-Frame-Options SAMEORIGIN on the login page and admin screens, but not on front-end pages. You need to add headers yourself, through your server config, a plugin, or code, to cover the whole site.

Using SAMEORIGIN or self rarely breaks anything. Using DENY or none can break features that frame your own pages, like the WordPress Customizer preview, so test after making changes.

No. Browsers ignore frame-ancestors when it's delivered in a meta tag. It must be sent as an HTTP response header.

Yes, for sites that don't send framing headers. The fix is simple and well supported, so there's little reason not to apply it everywhere.


Conclusion

Clickjacking tricks visitors into clicking hidden elements of your site by loading it inside an invisible or disguised frame on an attacker's page. Because the victim is often logged in, those clicks can change settings, delete data, approve permissions, or make purchases, all without the user realizing what they've done.

Preventing it is one of the easiest security wins available. Send Content-Security-Policy: frame-ancestors 'self' (or 'none') along with X-Frame-Options: SAMEORIGIN (or DENY) from your server, CDN, or application, test that framing is blocked, and add confirmation steps for sensitive actions. A few lines of configuration close the door on this entire class of attack.

Tags :
Share :

Related Posts

What are the best WordPress security plugins?

What are the best WordPress security plugins?

The best WordPress security plugins for most sites are Wordfence, Sucuri Security, Solid Security, MalCare, All-In-One Security (AIOS), Patchstack, a

Dive Deeper
What are the most common website security threats?

What are the most common website security threats?

The most common website security threats are vulnerable or outdated software, weak and stolen passwords, malware infections, injection attacks like S

Dive Deeper
How does GDPR affect website security?

How does GDPR affect website security?

GDPR affects website security by turning it from a good habit into a legal obligation. If your website collects personal data from people in the EU (

Dive Deeper