Clickjacking Prevention in Python — Core Concepts
How clickjacking works
A clickjacking attack (also called UI redress) loads a target website inside an invisible <iframe> on an attacker-controlled page. The attacker positions the iframe so that specific buttons or links on the target page align with enticing elements on the visible page. When the victim clicks what they think is a harmless button, they’re actually interacting with the hidden target site.
Because the victim’s browser handles the iframe as a legitimate request — with all the victim’s cookies and authentication — the action succeeds. Real-world clickjacking attacks have tricked users into liking Facebook pages, enabling webcams, transferring money, and changing account settings.
The two defense headers
X-Frame-Options is the older, simpler header. It has three values:
DENY— the page cannot be displayed in any frame, period.SAMEORIGIN— the page can only be framed by pages on the same origin.ALLOW-FROM https://trusted.com— only this specific origin can frame it (but this value is not supported by modern Chrome or Firefox).
Content-Security-Policy: frame-ancestors is the modern replacement. It’s more flexible:
frame-ancestors 'none'— equivalent to DENY.frame-ancestors 'self'— equivalent to SAMEORIGIN.frame-ancestors 'self' https://partner.com— allow framing from your own site and a specific partner.
When both headers are present, frame-ancestors takes precedence in browsers that support it.
How Python frameworks handle it
Django enables X-Frame-Options: DENY by default through the XFrameOptionsMiddleware. It’s in the default middleware stack — you’d have to actively remove it to be vulnerable. You can change it globally or per-view:
# settings.py — change the global default
X_FRAME_OPTIONS = "SAMEORIGIN"
# Or per-view with a decorator
from django.views.decorators.clickjacking import xframe_options_sameorigin
@xframe_options_sameorigin
def embeddable_widget(request):
return render(request, "widget.html")
Flask doesn’t add frame protection by default. You need to add it yourself or use Flask-Talisman:
from flask_talisman import Talisman
Talisman(app, content_security_policy={
"frame-ancestors": "'self'"
})
FastAPI requires explicit middleware. Adding security headers is typically done through a custom middleware or a library.
When you need to allow framing
Some pages are designed to be embedded: widgets, payment forms, embedded players. For these, you need a selective approach rather than disabling protection entirely.
Use CSP frame-ancestors to whitelist specific trusted domains. Apply it only to the routes that need embedding, and keep DENY or 'none' as the default for everything else.
Common misconception
Developers sometimes think that clickjacking only matters for pages with “dangerous” actions like delete or transfer buttons. In reality, any interactive element can be targeted. Clickjacking has been used to trick users into enabling their microphone in browser permission dialogs, following accounts on social media, and even dragging text (like auth tokens) from one page to another.
Testing for clickjacking
Create a simple HTML file to test whether your site can be framed:
<iframe src="https://your-python-app.com/dashboard"
width="800" height="600"></iframe>
Open this in a browser. If the dashboard loads inside the frame, you’re vulnerable. If the browser blocks it or shows a blank frame, your protection is working.
Browser developer tools will also show the relevant error in the console when a frame is blocked.
The one thing to remember: Set X-Frame-Options: DENY or Content-Security-Policy: frame-ancestors 'none' on all pages by default, and only relax it for specific routes that genuinely need to be embedded in trusted sites.
See Also
- Python Api Key Management Why apps use special passwords called API keys, and how to keep them safe — explained with a library card analogy
- Python Attribute Based Access Control How apps make fine-grained permission decisions based on who you are, what you're accessing, and the circumstances — explained with an airport analogy
- Python Audit Logging Learn Audit Logging with a clear mental model so your Python code is easier to trust and maintain.
- Python Bandit Security Scanning Why Bandit Security Scanning helps Python teams catch painful mistakes early without slowing daily development.
- Python Content Security Policy How websites create a guest list for scripts and styles to block hackers from sneaking in malicious code