Syllabus Lesson 68 of 156 · Content Security Policy and framing
Content Security Policy and framing

Clickjacking

Why it matters If any site may show your page inside a frame, it can hide your page under its own and trick visitors into clicking your buttons: "delete account", "share my location", "send money". The visitor is really logged in to your site, so the click is real. One header stops it.

Picture a sheet of glass with a painted "Claim your prize" button, laid over a real bank form so that the painted button sits exactly on top of "Confirm transfer". You press the prize; your finger goes through the glass. That is clickjacking.

Clickjacking: an attack where a page loads another site in an invisible or disguised frame (an <iframe>) and lines up that site's buttons under its own, so the visitor's clicks land on the hidden page.

Framing: one page showing another inside an iframe. The page inside is the framed page; the page around it is its ancestor.

In the practice world, prize.example is the attacker page and bank.example/transfer is the target. The defence belongs to the bank: it must tell browsers who may frame its pages. The visitor cannot see the trick, and the attacker will not stop by itself.

The modern way: frame-ancestors

Content-Security-Policy: frame-ancestors 'none'        nobody may frame this page
Content-Security-Policy: frame-ancestors 'self'        only pages from this same origin
Content-Security-Policy: frame-ancestors https://learn.floati.life   only that one site
  • It checks every ancestor, not only the direct parent, so an attacker cannot wrap a friendly page around yours.
  • It does not fall back to default-src. A policy without it does not restrict framing at all.
  • It works only as a header, not in a <meta> tag.

The old way: X-Frame-Options

X-Frame-Options: an older, separate header with two useful values, DENY (like 'none') and SAMEORIGIN (like 'self').

Why is it "the old way"? It cannot name another site: its ALLOW-FROM value was never supported everywhere and modern browsers ignore it. It was never part of CSP, so it has no report-only mode. And when a page sends both, browsers that understand frame-ancestors follow it and ignore X-Frame-Options. Many sites still send X-Frame-Options as well, as a backup for very old browsers. That is harmless.

A real example: this site

floati.life sends X-Frame-Options: DENY on every page, through Django's clickjacking middleware (checked October 2026). One page is the exception: a small "bridge" page that learn.floati.life loads in a hidden frame to carry a learner's progress across when the course moved. X-Frame-Options cannot say "only learn.floati.life", so that one view is exempted from it and sends frame-ancestors https://learn.floati.life instead. That is exactly the case the old header cannot handle.

@xframe_options_exempt
def bridge(request):
    response = render(request, "bridge.html")
    response._csp_update = {"frame-ancestors": [settings.LEARN_ORIGIN]}
    return response

Belt and braces

Framing control is the real fix. Two further habits help: ask for confirmation, or a password, before very sensitive actions, and set session cookies to SameSite=Lax or Strict (Lesson 10), so a framed page from another site often is not logged in at all.

A common misconception: "my page has no secret data, so framing does not matter". Clickjacking is about stealing clicks, not reading data. Any button that changes something is a target.

Try it In DevTools, check the response headers of a site you use for x-frame-options and for frame-ancestors inside content-security-policy. Then think of its most important button: could a hidden frame aim at it?

Recap

  • Clickjacking hides your page in a frame and steals real clicks from logged-in visitors.
  • Send frame-ancestors 'none' (or 'self', or named sites); it checks every ancestor and never falls back to default-src.
  • X-Frame-Options is the old way: it cannot name another site and is ignored when frame-ancestors is present.

Spotted a problem in this lesson? Report it

Code · runs in your browser
Output