How to Prevent Clickjacking on Websites

How to Prevent Clickjacking on Websites

A visitor clicks a harmless-looking button, but an invisible page captures the action. That single click could change an email address, approve a payment, or alter an account setting.

When I assess how to prevent clickjacking on websites, I never depend on one control. I use Content Security Policy as the main defense, retain a legacy header, secure session cookies, and test every sensitive route.

What Is a Clickjacking Attack?

Clickjacking is a user-interface attack in which a malicious site places another webpage inside a transparent or disguised frame. The victim believes they are clicking the visible page. Their click actually reaches a button or link on the framed website.

OWASP describes this technique as a UI redress attack. Attackers use transparent or opaque layers to redirect clicks toward another application or domain.

A successful attack often requires three conditions:

  • The target page can load inside an iframe.
  • The victim has an active session on the target site.
  • The framed page contains an action worth triggering.

Account settings, administrative panels, checkout screens, and consent controls deserve close attention.

How to Prevent Clickjacking on Websites With Layered Defenses

How to Prevent Clickjacking on Websites With Layered Defenses

The strongest approach removes the attacker’s ability to frame sensitive pages. Supporting controls then reduce risk if a configuration gap remains.

1. Block Unauthorized Framing With CSP

The frame-ancestors directive in Content Security Policy should be my first control. It tells the browser which parent pages may embed a document.

To block every site from framing a page, I use:

Content-Security-Policy: frame-ancestors ‘none’;

For pages that must appear inside frames on the same origin, I use:

Content-Security-Policy: frame-ancestors ‘self’;

A legitimate partner can be added explicitly:

Content-Security-Policy: frame-ancestors ‘self’ https://trustedpartner.example;

OWASP recommends frame-ancestors because it can authorize multiple permitted origins through CSP rules. MDN also identifies it as an effective defense because clickjacking depends on embedding the target document.

I send this policy through the HTTP response header. The directive does not work when placed inside a CSP <meta> element. It also does not inherit from default-src, so I must declare it directly.

2. Add X-Frame-Options for Legacy Support

2. Add X-Frame-Options for Legacy Support

I also send X-Frame-Options when a page does not require complex partner framing. It provides fallback protection for browsers or systems that do not enforce modern CSP rules correctly.

Two valid directives remain useful:

X-Frame-Options: DENY

DENY blocks framing from every origin, including the website’s own origin.

X-Frame-Options: SAMEORIGIN

SAMEORIGIN permits framing only when the parent shares the same origin.

MDN confirms that the header controls rendering inside frame, iframe, embed, and object elements. I avoid the obsolete ALLOW-FROM directive because modern browsers do not support it consistently.

For a page that should never be framed, I pair the headers:

Content-Security-Policy: frame-ancestors ‘none’;

X-Frame-Options: DENY

For same-origin framing, I use:

Content-Security-Policy: frame-ancestors ‘self’;

X-Frame-Options: SAMEORIGIN

3. Protect Session Cookies With SameSite

Clickjacking becomes more damaging when the framed page recognizes an authenticated user. A session cookie may allow the hidden action to run with the victim’s permissions.

I configure session cookies like this when the application permits strict same-site behavior:

Set-Cookie: session_id=xyz123; Secure; HttpOnly; SameSite=Strict

SameSite=Strict provides the strongest cross-site restriction but may interrupt legitimate external sign-in or navigation flows. SameSite=Lax is often a more practical choice for ordinary web sessions.

MDN recommends Lax or Strict session cookies as part of its clickjacking defense checklist. However, I treat SameSite as supporting protection, not a replacement for framing controls.

Cookie security also depends on HTTPS. Teams dealing with cookie or transport errors should know how to fix mixed content warnings before enforcing production settings across the application.

4. Use Frame-Busting Code Only as a Fallback

4. Use Frame-Busting Code Only as a Fallback

JavaScript frame-busting detects whether a page is running as the top-level document. A basic script may hide the page until it confirms that no parent frame exists.

<style id=”antiClickjack”>

  body { display: none !important; }

</style>

<script>

  if (self === top) {

    document.getElementById(“antiClickjack”).remove();

  } else {

    top.location = self.location;

  }

</script>

I use this only when an old environment cannot process the required headers. Attackers may disable scripts, restrict top-level navigation, or use iframe sandbox settings to weaken JavaScript defenses.

Security headers are enforced by the browser outside the page’s script context. That makes them more dependable than frame-busting code.

A Practical Clickjacking Protection Example

Consider an application with public articles, an account dashboard, and a payment widget embedded by one approved partner.

Applying one global DENY rule would protect the dashboard but break the payment integration. Allowing all framing would keep the widget working but expose the account area.

I would separate the policies by route:

/account/*

Content-Security-Policy: frame-ancestors ‘none’;

X-Frame-Options: DENY

/payments/widget

Content-Security-Policy: frame-ancestors https://checkout.partner.example;

/articles/*

Content-Security-Policy: frame-ancestors ‘self’;

X-Frame-Options: SAMEORIGIN

This route-based model is my preferred original safeguard. It protects sensitive actions without forcing every page into the same framing rule.

How to Configure Clickjacking Headers

How to Configure Clickjacking Headers

The header must appear on the final HTML response. Adding it only to static assets, redirect responses, or selected templates leaves gaps.

Apache Configuration

Header always set Content-Security-Policy “frame-ancestors ‘none’;”

Header always set X-Frame-Options “DENY”

The mod_headers module must be enabled. I use always so error responses also receive the headers where supported.

Nginx Configuration

add_header Content-Security-Policy “frame-ancestors ‘none’;” always;

add_header X-Frame-Options “DENY” always;

After editing the configuration, I validate it before reloading Nginx.

Application-Level Configuration

Applications can set the headers through middleware. This approach works well when public, administrative, and embedded routes require different policies.

I still inspect the final response because a reverse proxy, CDN, load balancer, or hosting platform may remove or overwrite application headers.

How to Test Your Clickjacking Protection

Knowing how to prevent clickjacking on websites includes proving that the browser rejects unauthorized frames.

I begin by inspecting the response:

curl -I https://example.com/account

The output should contain the intended CSP and X-Frame-Options values.

Next, I create a local test page:

<!doctype html>

<html lang=”en”>

<body>

  <iframe

    src=”https://example.com/account”

    width=”900″

    height=”600″>

  </iframe>

</body>

</html>

I open the page from an unauthorized origin and check the browser console. The protected document should not render.

I then repeat the test for every approved integration. This second test matters. A policy can stop attackers while also blocking a legitimate checkout, support portal, or embedded dashboard.

My release check covers:

  • Logged-in and logged-out routes
  • Error and authentication pages
  • Mobile and desktop browsers
  • CDN-served and origin-served responses
  • Authorized partner frames
  • Nested frames with multiple ancestors

CSP checks every ancestor in a nested frame chain. If one ancestor is not permitted, the browser cancels the load.

Common Clickjacking Prevention Mistakes

The first mistake is setting frame-src instead of frame-ancestors. frame-src controls which framed content your page may load. It does not control who may frame your page.

Another mistake is placing frame-ancestors in a meta tag. Browsers require it in the HTTP response header.

I also avoid applying protection only to the home page. Attackers usually target authenticated actions, not marketing pages.

Finally, I do not describe SameSite cookies or JavaScript as complete clickjacking solutions. They reduce exposure but do not replace browser-enforced framing restrictions.

Frequently Asked Questions

1. What CSP directive prevents clickjacking?

Use Content-Security-Policy: frame-ancestors ‘none’; to block all framing or ‘self’ to permit only same-origin framing.

2. Is X-Frame-Options still necessary?

It remains a useful fallback, but CSP frame-ancestors offers better control and should serve as the primary defense.

3. Can SameSite cookies stop clickjacking?

SameSite cookies can limit authenticated cross-site requests, but they cannot replace CSP and X-Frame-Options headers.

4. How can I check whether my website is vulnerable?

Embed a sensitive page from another origin and inspect its response headers, browser console, authenticated routes, and approved integrations.

Lock the Frame Before Attackers Steal the Show

The practical answer to how to prevent clickjacking on websites is layered enforcement. I begin with CSP frame-ancestors, add a compatible X-Frame-Options value, protect session cookies, and reserve JavaScript for legacy fallback use.

My final step is always route-by-route testing. Check your account, checkout, administration, and consent pages first. One missing header on a sensitive endpoint can undermine an otherwise strong configuration.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *