skip to content

In Django, how does XFrameOptionsMiddleware protect against clickjacking, and how do you let one view be framed by your own pages?

level: middleimportance: should knowfreq 40%

answer

  1. DENY by default
  2. a site-wide setting
  3. three view decorators
  4. an existing header wins

basics

~10 s

XFrameOptionsMiddleware adds X-Frame-Options with the X_FRAME_OPTIONS value, DENY by default, so browsers refuse to frame pages. Decorate one view with @xframe_options_sameorigin to allow same-origin framing, or @xframe_options_exempt to skip the header.

solid answer

~30 s

`django.middleware.clickjacking.XFrameOptionsMiddleware`, in the default `MIDDLEWARE`, sets `X-Frame-Options` on every response to `X_FRAME_OPTIONS`, which defaults to `'DENY'`: no site may frame the page, so an attacker cannot overlay it invisibly and trick clicks. It skips responses that already carry the header or were marked with `@xframe_options_exempt`. For one view that my own pages embed, I use `@xframe_options_sameorigin` from `django.views.decorators.clickjacking`, which sets `SAMEORIGIN` when the header is absent, leaving the rest of the site on `DENY`. `@xframe_options_deny` pins `DENY` regardless of the setting, and `@xframe_options_exempt` sends no header, which I use only when another mechanism, such as a CSP `frame-ancestors` policy, controls framing.

code

python · 10 lines
python
from django.shortcuts import get_object_or_404, render
from django.views.decorators.clickjacking import xframe_options_sameorigin

from reports.models import Report


@xframe_options_sameorigin
def report_preview(request, pk):
    report = get_object_or_404(Report, pk=pk)
    return render(request, "reports/preview.html", {"report": report})

go deeper

for a junior

Recall that XFrameOptionsMiddleware sends X-Frame-Options: DENY by default and that it defends against clickjacking.

for a middle

Explain the middleware's skip rules and the three decorators, and when to use SAMEORIGIN versus exempt.

for a senior

Keep DENY globally, relax only specific views, never exempt pages with actions, and pair exemptions with a frame-ancestors policy.

for a principal

Decide which content is embeddable at all and who approves new embedding partners.

## The attack **Clickjacking** loads a victim site inside an invisible `<iframe>` on an attacker's page and positions it so that the user, thinking they are clicking the attacker's button, actually clicks a button on the victim site: a delete, a payment confirmation, a permission grant. The defence is to tell browsers not to render the page inside frames from other sites. ## Django's default protection `django.middleware.clickjacking.XFrameOptionsMiddleware` is part of the `MIDDLEWARE` list that `startproject` generates. On every response it: 1. returns the response unchanged if it already has an `X-Frame-Options` header; 2. returns it unchanged if the view marked it with `@xframe_options_exempt`; 3. otherwise sets `X-Frame-Options` to the `X_FRAME_OPTIONS` setting, upper-cased. `X_FRAME_OPTIONS` defaults to `'DENY'`, so out of the box no page can be framed, not even by the same site. The other value in practical use is `'SAMEORIGIN'`, which allows framing by pages from the same origin. The header is set by this middleware, not by `SecurityMiddleware`; removing `XFrameOptionsMiddleware` from `MIDDLEWARE` removes the protection. ## Per-view control `django.views.decorators.clickjacking` provides three decorators, each working on sync and async views: | Decorator | Effect on that view's responses | |---|---| | `@xframe_options_sameorigin` | sets `X-Frame-Options: SAMEORIGIN` if the header is not already set | | `@xframe_options_deny` | sets `X-Frame-Options: DENY` if the header is not already set | | `@xframe_options_exempt` | marks the response so the middleware adds no header at all | Because the first two set the header before the middleware runs, the middleware sees it and leaves it alone. This lets a site keep `DENY` globally and relax exactly one view, for example a print preview or an embeddable chart that the site shows in its own dashboard frame. ## Choosing between SAMEORIGIN and exempt - **Framed by your own pages:** `@xframe_options_sameorigin`. The rest of the site stays on `DENY`. - **Framed by a specific partner site:** `X-Frame-Options` cannot express an allow-list, so exempt the view and control framing with a Content Security Policy `frame-ancestors` directive, which Django 6.x can send through its CSP support. Choosing between the two headers is a browser-security topic; the Django point is that `@xframe_options_exempt` must not be used alone. - **Meant to be embedded anywhere:** `@xframe_options_exempt`, only for content with no state-changing actions, such as a public widget. ## Common mistakes - **Switching the whole site to `SAMEORIGIN`** because one view needs it. Use the decorator instead. - **Exempting a view that has forms or buttons.** That page is now clickjackable from any site. - **Assuming `SecurityMiddleware` handles framing.** It sends `nosniff`, `Referrer-Policy` and COOP, not `X-Frame-Options`. - **Class-based views.** Apply the decorators with `method_decorator` on `dispatch`, or wrap the view returned by `as_view()` in the URLconf. - **Expecting the setting to override a view's header.** An existing header always wins, including one set by a decorator. ## Testing framing rules Framing exceptions are easy to lose in a refactor, for example when a function view is rewritten as a class-based view and the decorator is dropped. A small test per exception keeps them visible: - request the relaxed view and assert `response.headers["X-Frame-Options"] == "SAMEORIGIN"`; - request an ordinary page and assert the header is `DENY`; - for an exempted view, assert `"X-Frame-Options" not in response.headers` and, if CSP controls framing, that the `frame-ancestors` directive is present. The test client runs the full middleware stack, so these assertions reflect what browsers receive. ## The admin and other built-ins Django's admin and other built-in views rely on the same middleware; they inherit the `DENY` default. If a project embeds admin pages in its own frames (rare), it needs `SAMEORIGIN` for those responses, and the safest route is a targeted exception rather than a global change.

  • What happens if a view sets X-Frame-Options itself and X_FRAME_OPTIONS is 'DENY'?
    The view's value is kept. `XFrameOptionsMiddleware` checks for an existing header first and returns the response unchanged if one is there, so a hand-set header, or one set by a decorator, always wins over the setting.
  • How do you apply xframe_options_sameorigin to a class-based view?
    Use `django.utils.decorators.method_decorator`, for example `@method_decorator(xframe_options_sameorigin, name="dispatch")` on the class, or wrap the `as_view()` result in the URLconf. The decorator then sets the header on every response the view returns.

saying these in an interview costs you the question

  • SecurityMiddleware is what sends X-Frame-Options.
  • X_FRAME_OPTIONS defaults to SAMEORIGIN.
  • To let one page be framed, switch the whole site to SAMEORIGIN.
  • @xframe_options_exempt is a safe way to allow a single partner site.
  • The X_FRAME_OPTIONS setting overrides a header the view already set.