Clickjacking

Tricking users into clicking things they can't see

How the attack works

Clickjacking diagramAn attacker's site sits on top of the victim's site. A benign-looking button on the attacker's page is aligned over a sensitive button on the victim's page.bank.example.comSecureBankAccount balance$4,820.00Transfer fundsvictim site (real, logged-in)win-a-prize.attacker.com🎉 You've been selected!Click below to claim yourfree gift card!invisible iframe(opacity: 0)Claim free gift! 🎁attacker site (what user sees)alignedHOW CLICKJACKING WORKS

An attacker builds a page that embeds your site inside an <iframe>. They make the iframe invisible - setting its opacity to zero - and position it precisely over something enticing on their own page: a prize claim button, a video play button, a CAPTCHA. The user thinks they're clicking the attacker's button. They're actually clicking whatever is underneath on your site - a "confirm payment" button, a "grant access" button, a "delete account" button.

The user is fully authenticated on your site (their session cookie is sent with the request as normal), so the action goes through. From the server's perspective it looks like a legitimate click from a logged-in user.

The name comes from "hijacking clicks" - the user's click is stolen and redirected to an interface they never intended to interact with. A related technique is cursorjacking- replacing the visible cursor image with one that appears offset from the real cursor position, so the user thinks they're clicking one thing while actually clicking another.

What attackers actually do with it

The attack is only useful for actions that happen in a single click - an attacker can't guide a user through a multi-step form they can't see. So the targets tend to be:

  • Liking, following, or sharing - the original "likejacking" on Facebook in 2010 spread virally this way.
  • Granting OAuth permissions - clicking "Allow" on a permission dialog.
  • Making purchases on sites with one-click buying.
  • Changing account settings - email address, password recovery options.
  • Initiating file downloads or camera/microphone permission grants.

Adobe Flash's settings panel was clickjacked to silently enable camera and microphone access. Twitter, Facebook, and various banking sites have all been targets at various points.

The defence

The root cause is that your site can be loaded inside an iframe on someone else's page. The fix is to tell browsers not to allow that.

There are two headers that do this. The older one is X-Frame-Options:

X-Frame-Options: DENY          # never allow framing
X-Frame-Options: SAMEORIGIN    # only allow framing by pages on the same origin

The modern replacement is the frame-ancestors directive in your Content Security Policy:

Content-Security-Policy: frame-ancestors 'none'         # equivalent to DENY
Content-Security-Policy: frame-ancestors 'self'         # equivalent to SAMEORIGIN
Content-Security-Policy: frame-ancestors https://partner.com  # allow a specific origin

frame-ancestors is more flexible - it lets you whitelist specific trusted origins - and it takes precedence over X-Frame-Options in browsers that support CSP. In practice, send both for maximum compatibility.

There is also a JavaScript-based defence called frame busting- code that detects if your page is inside a frame and breaks out of it. It was the main defence before these headers existed. It's brittle and bypassable (an attacker can use the sandbox attribute on the iframe to block JavaScript from running) and should not be relied on. Use the headers.

Django

Django's XFrameOptionsMiddleware sends X-Frame-Options: DENY by default. It's included in MIDDLEWARE in a standard Django project and requires no configuration to get basic protection.

# settings.py

MIDDLEWARE = [
    ...
    "django.middleware.clickjacking.XFrameOptionsMiddleware",
    ...
]

# Default is DENY. Change to SAMEORIGIN if you need to iframe your own pages:
X_FRAME_OPTIONS = "DENY"

If you need to allow a specific page to be framed - an embeddable widget, for example - you can override the header on a per-view basis:

from django.views.decorators.clickjacking import xframe_options_sameorigin

@xframe_options_sameorigin
def my_embeddable_view(request):
    ...

For frame-ancestorsvia CSP, you'd configure that through your CSP header setup - see the Content Security Policy article.

Worth knowing

  • Clickjacking requires the user to be logged in - it piggybacks on their session. If the sensitive action requires re-authentication (entering a password), clickjacking is defeated.
  • It only works for single-click actions. Anything requiring typed input is not vulnerable.
  • The defence is entirely server-side. There is nothing the user can do to protect themselves.
  • X-Frame-Options with ALLOWFROM (allow a specific origin) was never consistently supported across browsers - use CSP frame-ancestors for that use case instead.
Read nextContent Security PolicyCSP's frame-ancestors directive is the modern replacement for X-Frame-Options, and CSP covers a much broader range of injection attacks too.