Skip to content

Clickjacking Protection in .htaccess

Learn how to prevent clickjacking attacks by configuring X-Frame-Options in Apache's .htaccess and comparing it with modern CSP frame-ancestors protection.

Clickjacking is a deceptively simple attack that can trick your users into performing actions they never intended. An attacker loads your website inside an invisible iframe on their own malicious page, positions it over a tempting button like "Win a Prize," and when the user clicks, they are actually clicking a button on your site. That click could transfer money, change account settings, delete data, or grant permissions without the user ever realizing what happened.

This guide explains how clickjacking works, how to protect against it using the X-Frame-Options header, the differences between its available values, and how it compares to the modern CSP frame-ancestors directive.

What Is Clickjacking

Clickjacking (also called UI redress attack) exploits the browser's ability to layer web pages on top of each other using iframes. The attacker creates a page that loads your site in a transparent iframe positioned precisely over interactive elements on the attacker's visible page.

Here is a simplified example of how an attack works:

Attacker's malicious pagehtml
<html>
<head><title>You Won a Prize!</title></head>
<body>
  <h1>Click the button to claim your free gift!</h1>
  <button style="font-size: 24px; padding: 20px;">Claim Prize</button>

  <!-- Your site loaded invisibly on top -->
  <iframe src="https://your-bank.com/transfer?to=attacker&amount=1000"
          style="opacity: 0; position: absolute; top: 0; left: 0;
                 width: 100%; height: 100%; z-index: 999;">
  </iframe>
</body>
</html>

The user sees a "Claim Prize" button. But the invisible iframe containing your banking site is layered on top. When the user clicks what they think is the prize button, they are actually clicking the "Confirm Transfer" button on the bank's page. Because the user is already logged into the bank (their session cookie is sent automatically), the transfer goes through.

What Makes Clickjacking Dangerous

Several factors make clickjacking particularly effective:

  • No special vulnerability needed. The attack does not require a bug in your code. It exploits normal browser behavior.
  • The user is genuinely interacting. Unlike CSRF attacks that forge requests programmatically, clickjacking involves the user's real mouse click, which can bypass some anti-CSRF protections.
  • Difficult to detect. The user sees the attacker's page, not yours. They have no visual indication that anything is wrong.
  • Multiple clicks can be chained. Sophisticated attacks guide users through multi-step processes by moving the invisible iframe between clicks.

Common targets for clickjacking include:

Target Page Potential Damage
Banking transfer confirmation Unauthorized money transfers
Social media "Follow" or "Like" Inflated engagement, spam distribution
Account settings pages Email or password changes
Permission grant dialogs Camera, microphone, or notification access
One-click purchase buttons Unauthorized purchases
Admin panel actions User deletion, configuration changes

X-Frame-Options Header

The X-Frame-Options HTTP response header is the traditional defense against clickjacking. It tells the browser whether your page is allowed to be displayed inside a <frame>, <iframe>, <object>, or <embed> element.

The header is set in .htaccess using mod_headers:

<IfModule mod_headers.c>
  Header always set X-Frame-Options "DENY" "expr=%{CONTENT_TYPE} =~ m#text/html#i"
</IfModule>

The expression expr=%{CONTENT_TYPE} =~ m#text/html#i ensures the header is only added to HTML responses. There is no reason to send framing restrictions with images, CSS files, or API responses.

X-Frame-Options accepts three values. The header must contain exactly one of them.

DENY

<IfModule mod_headers.c>
  Header always set X-Frame-Options "DENY" "expr=%{CONTENT_TYPE} =~ m#text/html#i"
</IfModule>

DENY is the strictest setting. The page cannot be displayed in a frame under any circumstances, not even by your own site. No origin, no domain, no exception.

When a browser encounters X-Frame-Options: DENY on a response loaded inside an iframe, it refuses to render the content and typically shows a blank frame or an error message.

Use DENY when:

  • Your pages should never appear inside any iframe.
  • You do not embed your own pages within your site using iframes.
  • You want the simplest, most secure option.

This is the recommended default for most websites. The vast majority of web pages have no legitimate reason to be framed.

SAMEORIGIN

<IfModule mod_headers.c>
  Header always set X-Frame-Options "SAMEORIGIN" "expr=%{CONTENT_TYPE} =~ m#text/html#i"
</IfModule>

SAMEORIGIN allows the page to be framed, but only by pages from the same origin (same scheme, host, and port). Pages from any other origin are blocked from framing your content.

Framing page Your page Allowed?
https://example.com/main https://example.com/widget Yes
https://example.com/admin https://example.com/dashboard Yes
https://evil.com/trap https://example.com/checkout No
http://example.com/page https://example.com/widget No (different scheme)
https://sub.example.com/page https://example.com/widget No (different host)

Use SAMEORIGIN when:

  • Your site uses iframes to embed its own pages (e.g., a dashboard that loads widgets in iframes, a page builder with preview frames).
  • You need to frame your own content but want to prevent external sites from doing so.

ALLOW-FROM (Deprecated)

Header always set X-Frame-Options "ALLOW-FROM https://trusted-partner.com"

ALLOW-FROM was intended to allow framing from a specific named origin. However, it has been deprecated and should not be used for the following reasons:

  • Chrome never supported it.
  • Firefox dropped support.
  • Edge (Chromium-based) does not support it.
  • Safari has inconsistent support.
  • Only legacy versions of Internet Explorer and old Edge recognized it.

If you send ALLOW-FROM to a browser that does not support it, the header is ignored entirely, leaving your page with no clickjacking protection at all.

Applying to Specific Pages

You may want different framing policies for different parts of your site. For example, your main pages should not be frameable, but a specific widget page needs to be embedded.

Block framing site-wide, then allow for specific files:

<IfModule mod_headers.c>
  Header always set X-Frame-Options "DENY" "expr=%{CONTENT_TYPE} =~ m#text/html#i"

  <Location "/embed/widget">
    Header always set X-Frame-Options "SAMEORIGIN"
  </Location>
</IfModule>

Or using <Files> for specific files:

<IfModule mod_headers.c>
  Header always set X-Frame-Options "DENY" "expr=%{CONTENT_TYPE} =~ m#text/html#i"

  <Files "embed.html">
    Header always set X-Frame-Options "SAMEORIGIN"
  </Files>
</IfModule>

Common Mistake: Forgetting the Content-Type Condition

Less optimal approach:

Header always set X-Frame-Options "DENY"

While this is not technically harmful (browsers only apply X-Frame-Options to document loads, not subresources), it adds unnecessary bytes to every response. On high-traffic sites serving thousands of static assets, this overhead is wasteful.

Better approach:

Header always set X-Frame-Options "DENY" "expr=%{CONTENT_TYPE} =~ m#text/html#i"

X-Frame-Options vs CSP frame-ancestors

The CSP frame-ancestors directive is the modern replacement for X-Frame-Options. It provides the same protection with significantly more flexibility.

Feature Comparison

Feature X-Frame-Options CSP frame-ancestors
Block all framing DENY frame-ancestors 'none'
Allow same-origin framing SAMEORIGIN frame-ancestors 'self'
Allow specific external domain ALLOW-FROM (broken) frame-ancestors https://partner.com
Allow multiple external domains Not possible frame-ancestors https://a.com https://b.com
Allow all subdomains of a domain Not possible frame-ancestors *.example.com
Browser support Universal (legacy) All modern browsers
Part of a broader security framework No (standalone header) Yes (part of CSP)

Equivalent Configurations

Block all framing:

<IfModule mod_headers.c>
  Header always set X-Frame-Options "DENY" "expr=%{CONTENT_TYPE} =~ m#text/html#i"

  Header always set Content-Security-Policy "frame-ancestors 'none'"
</IfModule>

Allow same-origin only:

<IfModule mod_headers.c>
  Header always set X-Frame-Options "SAMEORIGIN" "expr=%{CONTENT_TYPE} =~ m#text/html#i"
  Header always set Content-Security-Policy "frame-ancestors 'self'"
</IfModule>

Allow specific external domains (only possible with CSP):

<IfModule mod_headers.c>
  Header always set Content-Security-Policy "frame-ancestors 'self' https://trusted-partner.com https://another-partner.com"
</IfModule>

Should You Use Both?

Yes, use both headers together. This is the recommended approach for maximum compatibility:

Recommended: Both headers for maximum protectionapache
<IfModule mod_headers.c>
  Header always set Content-Security-Policy "frame-ancestors 'none'"

  Header always set X-Frame-Options "DENY" "expr=%{CONTENT_TYPE} =~ m#text/html#i"
</IfModule>

When both headers are present, modern browsers use frame-ancestors and ignore X-Frame-Options. Older browsers that do not understand CSP use X-Frame-Options as a fallback. This layered approach ensures protection across all browser versions.

When frame-ancestors Is the Only Option

There are scenarios where X-Frame-Options simply cannot solve the problem:

Allowing multiple specific partners to embed your content:

<IfModule mod_headers.c>
  Header always set Content-Security-Policy "frame-ancestors 'self' https://partner-a.com https://partner-b.com https://partner-c.com"

  Header always set X-Frame-Options "SAMEORIGIN" "expr=%{CONTENT_TYPE} =~ m#text/html#i"
</IfModule>

Allowing all subdomains of a partner domain:

<IfModule mod_headers.c>
  Header always set Content-Security-Policy "frame-ancestors 'self' https://*.partner-network.com"
</IfModule>

Complete Production Configuration

Here is a production-ready clickjacking protection setup:

Complete clickjacking protectionapache
<IfModule mod_headers.c>
  Header always set Content-Security-Policy "frame-ancestors 'none'"

  Header always set X-Frame-Options "DENY" "expr=%{CONTENT_TYPE} =~ m#text/html#i"
</IfModule>

If you already have a broader CSP policy, add frame-ancestors to it rather than creating a second Content-Security-Policy header:

frame-ancestors as part of a broader CSPapache
<IfModule mod_headers.c>
  Header always set Content-Security-Policy "\
    default-src 'self'; \
    script-src 'self' https://cdn.example.com; \
    style-src 'self' 'unsafe-inline'; \
    frame-ancestors 'none'"

  Header always set X-Frame-Options "DENY" "expr=%{CONTENT_TYPE} =~ m#text/html#i"
</IfModule>

Clickjacking protection is straightforward to implement and has virtually no downside for the vast majority of websites. Set X-Frame-Options: DENY and frame-ancestors 'none' as your default, and only relax the policy for specific pages that genuinely need to be embedded. This simple header combination eliminates an entire class of attacks with a single line of configuration.

Navigation