Security

Clickjacking Protection

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.

Clickjacking Protection

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.

Understanding Clickjacking Attacks

How Clickjacking Works

<!-- 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>

Clickjacking Attack Scenarios

# 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's Clickjacking Protection

X-Frame-Options Middleware

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)

How X-Frame-Options Works

# 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)

Per-View Frame Options

# 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

Content Security Policy (CSP) Protection

CSP Frame Ancestors

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

Dynamic CSP Policies

# 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')

Advanced Clickjacking Protection

JavaScript-Based Protection

// 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 '';
    }
})();

Server-Side Frame Detection

# 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)

Secure Iframe Implementation

When Framing is Necessary

# 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)

Secure Widget Template

<!-- 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>

Testing Clickjacking Protection

Security Tests

# 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])

Best Practices Summary

Protection Strategies

  • Use X-Frame-Options: DENY for sensitive pages
  • Implement CSP frame-ancestors for modern browsers
  • Add JavaScript frame busting for additional protection
  • Validate embedding context for legitimate widgets

Configuration Guidelines

  • Set X_FRAME_OPTIONS = 'DENY' as default
  • Use SAMEORIGIN only when necessary for legitimate embedding
  • Implement CSP policies with appropriate frame-ancestors
  • Monitor and log potential clickjacking attempts

Development Practices

  • Test all sensitive actions for clickjacking vulnerability
  • Implement proper referrer validation for embeddable content
  • Use secure coding practices for iframe content
  • Regular security audits and penetration testing

Monitoring and Response

  • Log all framing attempts and suspicious referrers
  • Monitor for unusual iframe-related traffic patterns
  • Implement alerting for potential clickjacking attacks
  • Regular review of embedding policies and trusted domains

Next Steps

Now that you understand clickjacking protection, let's explore HTTPS setup and HTTP Strict Transport Security (HSTS) to ensure secure communications in Django applications.