In Django, how does XFrameOptionsMiddleware protect against clickjacking, and how do you let one view be framed by your own pages?
answer
- DENY by default
- a site-wide setting
- three view decorators
- an existing header wins
basics
~10 sXFrameOptionsMiddleware 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 linesfrom 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
Recall that XFrameOptionsMiddleware sends X-Frame-Options: DENY by default and that it defends against clickjacking.
Explain the middleware's skip rules and the three decorators, and when to use SAMEORIGIN versus exempt.
Keep DENY globally, relax only specific views, never exempt pages with actions, and pair exemptions with a frame-ancestors policy.
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.