Clickjacking is a malicious technique where attackers trick users into clicking on something different from what they perceive, potentially leading to unauthorized actions. Django provides built-in protection against clickjacking attacks through frame options and Content Security Policy headers.
<!-- Malicious website example -->
<!DOCTYPE html>
<html>
<head>
<title>Win a Free iPhone!</title>
<style>
.overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1000;
opacity: 0; /* Invisible overlay */
}
.fake-button {
position: absolute;
top: 200px;
left: 300px;
width: 200px;
height: 50px;
background: red;
color: white;
text-align: center;
line-height: 50px;
cursor: pointer;
}
.hidden-iframe {
position: absolute;
top: 150px; /* Positioned so real button aligns with fake button */
left: 250px;
width: 300px;
height: 100px;
opacity: 0.01; /* Nearly invisible but still functional */
z-index: 999;
}
</style>
</head>
<body>
<h1>Congratulations! You've won a free iPhone!</h1>
<p>Click the button below to claim your prize:</p>
<!-- Fake button that user sees -->
<div class="fake-button">Claim Prize!</div>
<!-- Hidden iframe containing the real application -->
<iframe src="https://yourapp.com/delete-account/"
class="hidden-iframe">
</iframe>
<!-- User thinks they're clicking "Claim Prize" but actually clicking "Delete Account" -->
</body>
</html>
# Common clickjacking targets in web applications:
# 1. Account deletion
# Attacker embeds: https://yourapp.com/delete-account/
# User thinks they're clicking: "Download Free Software"
# Actually clicking: "Confirm Account Deletion"
# 2. Money transfer
# Attacker embeds: https://bank.com/transfer/
# User thinks they're clicking: "Play Game"
# Actually clicking: "Transfer $1000"
# 3. Social media actions
# Attacker embeds: https://social.com/share/
# User thinks they're clicking: "See Funny Video"
# Actually clicking: "Share Malicious Content"
# 4. Admin actions
# Attacker embeds: https://yourapp.com/admin/users/delete/
# User thinks they're clicking: "View Report"
# Actually clicking: "Delete User Account"
# 5. OAuth authorization
# Attacker embeds: https://oauth.provider.com/authorize/
# User thinks they're clicking: "Continue Reading"
# Actually clicking: "Grant App Permissions"
Django includes built-in clickjacking protection:
# settings.py - Enable clickjacking protection (enabled by default)
MIDDLEWARE = [
'django.middleware.clickjacking.XFrameOptionsMiddleware',
# ... other middleware
]
# X-Frame-Options settings
X_FRAME_OPTIONS = 'DENY' # Default - prevents all framing
# Alternative options:
# X_FRAME_OPTIONS = 'SAMEORIGIN' # Allow framing from same origin
# X_FRAME_OPTIONS = 'ALLOW-FROM https://trusted-site.com' # Allow specific origin (deprecated)
# What Django's XFrameOptionsMiddleware does:
class XFrameOptionsMiddleware:
"""Simplified version of Django's middleware"""
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
response = self.get_response(request)
# Add X-Frame-Options header if not already present
if not response.get('X-Frame-Options'):
response['X-Frame-Options'] = settings.X_FRAME_OPTIONS
return response
# HTTP Response Headers:
# X-Frame-Options: DENY
# - Prevents the page from being displayed in any frame/iframe
#
# X-Frame-Options: SAMEORIGIN
# - Allows framing only from the same origin
#
# X-Frame-Options: ALLOW-FROM https://example.com
# - Allows framing only from specified origin (deprecated)
# views.py - Customize frame options per view
from django.views.decorators.clickjacking import (
xframe_options_deny,
xframe_options_sameorigin,
xframe_options_exempt
)
@xframe_options_deny
def sensitive_action_view(request):
"""View that should never be framed"""
if request.method == 'POST':
# Perform sensitive action (e.g., delete account)
request.user.delete()
return redirect('goodbye')
return render(request, 'confirm_delete.html')
@xframe_options_sameorigin
def embeddable_widget_view(request):
"""View that can be embedded in same-origin iframes"""
widget_data = get_widget_data(request.user)
return render(request, 'widget.html', {'data': widget_data})
@xframe_options_exempt
def public_embed_view(request):
"""View that can be embedded anywhere (use carefully!)"""
# This view allows framing from any origin
# Only use for truly public, non-sensitive content
public_data = get_public_data()
return render(request, 'public_embed.html', {'data': public_data})
# Class-based views
from django.utils.decorators import method_decorator
@method_decorator(xframe_options_deny, name='dispatch')
class SensitiveFormView(FormView):
"""Form view with clickjacking protection"""
template_name = 'sensitive_form.html'
form_class = SensitiveActionForm
def form_valid(self, form):
# Perform sensitive action
form.execute_action(self.request.user)
return super().form_valid(form)
@method_decorator(xframe_options_sameorigin, name='dispatch')
class DashboardWidgetView(TemplateView):
"""Dashboard widget that can be embedded in same origin"""
template_name = 'dashboard_widget.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['widget_data'] = self.get_widget_data()
return context
CSP provides more modern and flexible clickjacking protection:
# middleware.py - CSP-based clickjacking protection
class CSPClickjackingMiddleware:
"""Content Security Policy middleware for clickjacking protection"""
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
response = self.get_response(request)
# Build CSP policy
csp_policy = self.build_csp_policy(request)
response['Content-Security-Policy'] = csp_policy
return response
def build_csp_policy(self, request):
"""Build CSP policy with frame-ancestors directive"""
# Base policy directives
directives = [
"default-src 'self'",
"script-src 'self' 'unsafe-inline'",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https:",
]
# Frame ancestors directive (replaces X-Frame-Options)
frame_ancestors = self.get_frame_ancestors_policy(request)
directives.append(f"frame-ancestors {frame_ancestors}")
return '; '.join(directives)
def get_frame_ancestors_policy(self, request):
"""Determine frame-ancestors policy based on request"""
# Sensitive pages - no framing allowed
sensitive_paths = [
'/delete-account/',
'/transfer-money/',
'/admin/',
'/change-password/'
]
if any(request.path.startswith(path) for path in sensitive_paths):
return "'none'" # Equivalent to X-Frame-Options: DENY
# Widget pages - same origin only
widget_paths = ['/widget/', '/embed/']
if any(request.path.startswith(path) for path in widget_paths):
return "'self'" # Equivalent to X-Frame-Options: SAMEORIGIN
# Public embeddable content
public_paths = ['/public-embed/']
if any(request.path.startswith(path) for path in public_paths):
return "'self' https://trusted-partner.com"
# Default policy
return "'self'"
# Alternative: Using django-csp package
# pip install django-csp
# settings.py
MIDDLEWARE = [
'csp.middleware.CSPMiddleware',
# ... other middleware
]
# CSP settings for clickjacking protection
CSP_FRAME_ANCESTORS = ("'none'",) # Deny all framing
# CSP_FRAME_ANCESTORS = ("'self'",) # Same origin only
# CSP_FRAME_ANCESTORS = ("'self'", "https://trusted-site.com") # Specific origins
# views.py - Dynamic CSP policies
from django.http import HttpResponse
def dynamic_csp_view(request):
"""View with dynamic CSP policy"""
# Determine CSP policy based on user or content
if request.user.is_staff:
# Staff users - more restrictive
csp_policy = "frame-ancestors 'none'"
elif request.GET.get('embed') == 'true':
# Embed mode - allow trusted partners
csp_policy = "frame-ancestors 'self' https://partner.example.com"
else:
# Regular users - same origin only
csp_policy = "frame-ancestors 'self'"
response = render(request, 'dynamic_content.html')
response['Content-Security-Policy'] = f"default-src 'self'; {csp_policy}"
return response
# Decorator for CSP policies
def csp_frame_ancestors(*ancestors):
"""Decorator to set CSP frame-ancestors policy"""
def decorator(view_func):
def wrapper(request, *args, **kwargs):
response = view_func(request, *args, **kwargs)
ancestors_str = ' '.join(ancestors)
csp = f"frame-ancestors {ancestors_str}"
# Add to existing CSP or create new one
existing_csp = response.get('Content-Security-Policy', '')
if existing_csp:
response['Content-Security-Policy'] = f"{existing_csp}; {csp}"
else:
response['Content-Security-Policy'] = f"default-src 'self'; {csp}"
return response
return wrapper
return decorator
# Usage
@csp_frame_ancestors("'none'")
def ultra_sensitive_view(request):
"""View that should never be framed"""
return render(request, 'ultra_sensitive.html')
@csp_frame_ancestors("'self'", "https://trusted-partner.com")
def partner_embeddable_view(request):
"""View that can be embedded by trusted partners"""
return render(request, 'partner_widget.html')
// static/js/clickjacking-protection.js
(function() {
'use strict';
// Frame busting code
function preventClickjacking() {
// Check if page is in a frame
if (window.top !== window.self) {
// Method 1: Break out of frame
try {
window.top.location = window.self.location;
} catch (e) {
// If we can't access parent, hide content
document.body.style.display = 'none';
// Show warning message
var warning = document.createElement('div');
warning.innerHTML = 'This page cannot be displayed in a frame for security reasons.';
warning.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:red;color:white;text-align:center;padding:50px;z-index:999999;';
document.body.appendChild(warning);
}
}
}
// Enhanced frame detection
function detectFraming() {
var isFramed = false;
try {
isFramed = (window.top !== window.self);
} catch (e) {
isFramed = true; // Cross-origin frame
}
if (isFramed) {
// Log potential clickjacking attempt
if (console && console.warn) {
console.warn('Potential clickjacking attempt detected');
}
// Send alert to server
fetch('/security/clickjacking-attempt/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCsrfToken()
},
body: JSON.stringify({
'referrer': document.referrer,
'user_agent': navigator.userAgent,
'timestamp': new Date().toISOString()
})
});
return true;
}
return false;
}
// Run protection on page load
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', preventClickjacking);
} else {
preventClickjacking();
}
// Continuous monitoring
setInterval(function() {
if (detectFraming()) {
preventClickjacking();
}
}, 1000);
// Helper function to get CSRF token
function getCsrfToken() {
var cookies = document.cookie.split(';');
for (var i = 0; i < cookies.length; i++) {
var cookie = cookies[i].trim();
if (cookie.indexOf('csrftoken=') === 0) {
return cookie.substring('csrftoken='.length);
}
}
return '';
}
})();
# views.py - Server-side clickjacking detection
import logging
logger = logging.getLogger('security')
class ClickjackingDetectionMixin:
"""Mixin to detect potential clickjacking attempts"""
def dispatch(self, request, *args, **kwargs):
"""Check for framing indicators"""
# Check for suspicious referrers
referrer = request.META.get('HTTP_REFERER', '')
if referrer and not self.is_trusted_referrer(referrer):
self.log_suspicious_referrer(request, referrer)
# Check for frame-related headers
if self.detect_framing_attempt(request):
self.log_framing_attempt(request)
return super().dispatch(request, *args, **kwargs)
def is_trusted_referrer(self, referrer):
"""Check if referrer is from trusted domain"""
from urllib.parse import urlparse
trusted_domains = [
request.get_host(),
'trusted-partner.com',
'widget.example.com'
]
referrer_domain = urlparse(referrer).netloc
return referrer_domain in trusted_domains
def detect_framing_attempt(self, request):
"""Detect potential framing based on request characteristics"""
# Check for frame-related headers
frame_headers = [
'HTTP_SEC_FETCH_DEST',
'HTTP_SEC_FETCH_MODE',
'HTTP_SEC_FETCH_SITE'
]
for header in frame_headers:
value = request.META.get(header, '')
if 'iframe' in value.lower() or 'nested-navigate' in value.lower():
return True
return False
def log_suspicious_referrer(self, request, referrer):
"""Log suspicious referrer"""
logger.warning(
"Suspicious referrer detected",
extra={
'ip_address': self.get_client_ip(request),
'referrer': referrer,
'path': request.path,
'user_agent': request.META.get('HTTP_USER_AGENT', ''),
'user': getattr(request, 'user', None),
}
)
def log_framing_attempt(self, request):
"""Log potential framing attempt"""
logger.warning(
"Potential clickjacking attempt detected",
extra={
'ip_address': self.get_client_ip(request),
'path': request.path,
'user_agent': request.META.get('HTTP_USER_AGENT', ''),
'headers': dict(request.META),
'user': getattr(request, 'user', None),
}
)
# Usage in views
class SensitiveActionView(ClickjackingDetectionMixin, FormView):
"""Sensitive view with clickjacking detection"""
template_name = 'sensitive_action.html'
form_class = SensitiveActionForm
@method_decorator(xframe_options_deny)
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
# views.py - Secure iframe implementation
from django.views.decorators.clickjacking import xframe_options_sameorigin
@xframe_options_sameorigin
def secure_widget_view(request):
"""Secure widget that can be embedded safely"""
# Validate embedding context
referrer = request.META.get('HTTP_REFERER', '')
if not validate_embedding_context(request, referrer):
return HttpResponseForbidden("Embedding not allowed from this context")
# Generate widget with security measures
widget_data = {
'content': get_widget_content(request.user),
'csrf_token': get_token(request),
'nonce': generate_nonce(),
}
response = render(request, 'secure_widget.html', widget_data)
# Add additional security headers
response['X-Content-Type-Options'] = 'nosniff'
response['Referrer-Policy'] = 'strict-origin-when-cross-origin'
return response
def validate_embedding_context(request, referrer):
"""Validate that embedding is from allowed context"""
if not referrer:
return False
from urllib.parse import urlparse
referrer_domain = urlparse(referrer).netloc
# Check against whitelist
allowed_domains = [
request.get_host(), # Same origin
'trusted-partner.com',
'widget.example.com'
]
return referrer_domain in allowed_domains
def generate_nonce():
"""Generate cryptographic nonce for CSP"""
import secrets
return secrets.token_urlsafe(16)
<!-- templates/secure_widget.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Secure Widget</title>
<!-- CSP with nonce -->
<meta http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self' 'nonce-{{ nonce }}'; frame-ancestors 'self' https://trusted-partner.com;">
<style>
/* Inline styles are safer than external CSS for widgets */
.widget-container {
border: 1px solid #ccc;
padding: 10px;
background: #f9f9f9;
font-family: Arial, sans-serif;
}
.widget-header {
font-weight: bold;
margin-bottom: 10px;
}
.security-indicator {
font-size: 12px;
color: #666;
margin-top: 10px;
}
</style>
</head>
<body>
<div class="widget-container">
<div class="widget-header">Secure Widget</div>
<div class="widget-content">
{{ content|escape }}
</div>
<div class="security-indicator">
🔒 This widget is served securely
</div>
</div>
<!-- Secure JavaScript with nonce -->
<script nonce="{{ nonce }}">
(function() {
'use strict';
// Verify we're in expected context
if (window.top !== window.self) {
// We're in a frame - verify it's allowed
try {
var parentOrigin = window.parent.location.origin;
var allowedOrigins = ['https://trusted-partner.com'];
if (allowedOrigins.indexOf(parentOrigin) === -1) {
console.warn('Widget loaded in unauthorized frame');
document.body.innerHTML = '<p>Widget cannot be displayed in this context</p>';
}
} catch (e) {
// Cross-origin frame - this is expected for legitimate embedding
}
}
// Widget functionality
function initializeWidget() {
// Safe widget initialization code
console.log('Secure widget initialized');
}
// Initialize when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializeWidget);
} else {
initializeWidget();
}
})();
</script>
</body>
</html>
# tests.py - Clickjacking protection tests
from django.test import TestCase, Client
from django.urls import reverse
class ClickjackingProtectionTests(TestCase):
"""Test clickjacking protection mechanisms"""
def setUp(self):
self.client = Client()
def test_x_frame_options_deny(self):
"""Test that X-Frame-Options: DENY is set"""
response = self.client.get(reverse('sensitive_action'))
self.assertEqual(response['X-Frame-Options'], 'DENY')
def test_x_frame_options_sameorigin(self):
"""Test that X-Frame-Options: SAMEORIGIN is set for widgets"""
response = self.client.get(reverse('widget_view'))
self.assertEqual(response['X-Frame-Options'], 'SAMEORIGIN')
def test_csp_frame_ancestors(self):
"""Test CSP frame-ancestors directive"""
response = self.client.get(reverse('csp_protected_view'))
csp = response.get('Content-Security-Policy', '')
self.assertIn('frame-ancestors', csp)
self.assertIn("'none'", csp)
def test_frame_busting_javascript(self):
"""Test that frame busting JavaScript is included"""
response = self.client.get(reverse('protected_page'))
self.assertContains(response, 'window.top !== window.self')
def test_embedding_validation(self):
"""Test that embedding validation works"""
# Test with no referrer
response = self.client.get(reverse('secure_widget'))
self.assertEqual(response.status_code, 403)
# Test with trusted referrer
response = self.client.get(
reverse('secure_widget'),
HTTP_REFERER='https://trusted-partner.com/page'
)
self.assertEqual(response.status_code, 200)
# Test with untrusted referrer
response = self.client.get(
reverse('secure_widget'),
HTTP_REFERER='https://malicious-site.com/attack'
)
self.assertEqual(response.status_code, 403)
class ClickjackingDetectionTests(TestCase):
"""Test clickjacking detection mechanisms"""
def test_suspicious_referrer_detection(self):
"""Test detection of suspicious referrers"""
with self.assertLogs('security', level='WARNING') as cm:
response = self.client.get(
reverse('sensitive_view'),
HTTP_REFERER='https://suspicious-site.com/frame-page'
)
self.assertIn('Suspicious referrer detected', cm.output[0])
def test_framing_attempt_detection(self):
"""Test detection of framing attempts"""
with self.assertLogs('security', level='WARNING') as cm:
response = self.client.get(
reverse('sensitive_view'),
HTTP_SEC_FETCH_DEST='iframe'
)
self.assertIn('Potential clickjacking attempt', cm.output[0])
Now that you understand clickjacking protection, let's explore HTTPS setup and HTTP Strict Transport Security (HSTS) to ensure secure communications in Django applications.
SQL Injection Protection
SQL injection is one of the most dangerous web application vulnerabilities, allowing attackers to manipulate database queries and potentially access, modify, or delete sensitive data. Django's ORM provides robust protection against SQL injection attacks through parameterized queries and safe query construction.
HTTPS Setup and HSTS
HTTPS (HTTP Secure) is essential for protecting data in transit and ensuring the integrity and confidentiality of communications between clients and servers. This chapter covers implementing HTTPS in Django applications and configuring HTTP Strict Transport Security (HSTS) for enhanced security.