Security

Cross Site Scripting

Cross-Site Scripting (XSS) is a vulnerability that allows attackers to inject malicious scripts into web pages viewed by other users. Django provides robust protection against XSS attacks through automatic template escaping and security best practices.

Cross Site Scripting

Cross-Site Scripting (XSS) is a vulnerability that allows attackers to inject malicious scripts into web pages viewed by other users. Django provides robust protection against XSS attacks through automatic template escaping and security best practices.

Understanding XSS Attacks

Types of XSS Attacks

<!-- 1. Reflected XSS - Script in URL parameters -->
<!-- Malicious URL: https://example.com/search?q=<script>alert('XSS')</script> -->

<!-- Vulnerable template (DON'T DO THIS) -->
<h1>Search results for: {{ request.GET.q|safe }}</h1>

<!-- 2. Stored XSS - Script stored in database -->
<!-- Malicious comment stored in database -->
<div class="comment">
    <!-- If user input contains: <script>steal_cookies()</script> -->
    {{ comment.content|safe }}  <!-- DANGEROUS! -->
</div>

<!-- 3. DOM-based XSS - Client-side script manipulation -->
<script>
    // Vulnerable JavaScript code
    var userInput = "{{ user_input|safe }}";  // DANGEROUS!
    document.getElementById('output').innerHTML = userInput;
</script>

XSS Attack Examples

# Example of vulnerable code (DON'T DO THIS)
def search_view(request):
    """VULNERABLE: Reflects user input without escaping"""
    query = request.GET.get('q', '')
    
    # This is dangerous - user input is not escaped
    html = f"<h1>Results for: {query}</h1>"
    
    return HttpResponse(html)

# Malicious input examples:
# ?q=<script>alert('XSS')</script>
# ?q=<img src=x onerror=alert('XSS')>
# ?q=javascript:alert('XSS')
# ?q=<svg onload=alert('XSS')>

def comment_view(request):
    """VULNERABLE: Stores unescaped user input"""
    if request.method == 'POST':
        content = request.POST.get('content')
        
        # Storing raw HTML is dangerous
        Comment.objects.create(
            user=request.user,
            content=content  # Could contain malicious scripts
        )
    
    comments = Comment.objects.all()
    return render(request, 'comments.html', {'comments': comments})

Django's XSS Protection

Automatic Template Escaping

Django automatically escapes variables in templates:

<!-- Django templates automatically escape by default -->
<div class="user-content">
    <!-- This is automatically escaped - SAFE -->
    <h2>{{ article.title }}</h2>
    <p>{{ article.content }}</p>
    
    <!-- User input is automatically escaped -->
    <div class="comment">
        Author: {{ comment.author.username }}
        Content: {{ comment.content }}
    </div>
</div>

<!-- What Django does automatically: -->
<!-- Input: <script>alert('XSS')</script> -->
<!-- Output: &lt;script&gt;alert(&#x27;XSS&#x27;)&lt;/script&gt; -->

Safe vs Unsafe Template Usage

<!-- SAFE: Default escaping -->
<div class="title">{{ article.title }}</div>
<!-- Input: <script>alert('XSS')</script> -->
<!-- Output: &lt;script&gt;alert(&#x27;XSS&#x27;)&lt;/script&gt; -->

<!-- UNSAFE: Using |safe filter (only use with trusted content) -->
<div class="content">{{ article.content|safe }}</div>
<!-- This bypasses escaping - only use with sanitized content! -->

<!-- SAFE: Using escape filter explicitly -->
<div class="user-input">{{ user_comment|escape }}</div>

<!-- SAFE: Using linebreaks filter (escapes then converts newlines) -->
<div class="formatted-text">{{ user_text|linebreaks }}</div>

<!-- UNSAFE: Using |safe with user input -->
<div class="dangerous">{{ request.GET.search|safe }}</div>  <!-- DON'T DO THIS! -->

Escaping in Different Contexts

<!-- HTML Content Context -->
<div>{{ user_input }}</div>  <!-- Automatically escaped -->

<!-- HTML Attribute Context -->
<input type="text" value="{{ user_input }}">  <!-- Automatically escaped -->
<img src="{{ image_url }}" alt="{{ image_description }}">

<!-- JavaScript Context - REQUIRES SPECIAL HANDLING -->
<script>
    // WRONG: This is not safe even with escaping
    var userInput = "{{ user_input }}";  // Still vulnerable!
    
    // CORRECT: Use JSON escaping
    var userInput = {{ user_input|escapejs }};
    
    // BETTER: Use json_script filter
    {{ user_data|json_script:"user-data" }}
    const userData = JSON.parse(document.getElementById('user-data').textContent);
</script>

<!-- CSS Context - Avoid user input in CSS -->
<style>
    /* DANGEROUS: Never put user input in CSS */
    .user-style { color: {{ user_color }}; }  /* DON'T DO THIS! */
</style>

<!-- URL Context -->
<a href="{{ user_url|urlencode }}">Link</a>  <!-- Use urlencode filter -->

Secure Template Practices

Using json_script for JavaScript Data

<!-- Secure way to pass data to JavaScript -->
{{ user_data|json_script:"user-data" }}
<script>
    const userData = JSON.parse(document.getElementById('user-data').textContent);
    
    // Now you can safely use userData in JavaScript
    console.log('User name:', userData.name);
    console.log('User email:', userData.email);
</script>
# views.py - Preparing data for json_script
def profile_view(request):
    """Secure way to pass data to JavaScript"""
    
    user_data = {
        'id': request.user.id,
        'name': request.user.get_full_name(),
        'email': request.user.email,
        'preferences': {
            'theme': request.user.profile.theme,
            'notifications': request.user.profile.notifications_enabled
        }
    }
    
    return render(request, 'profile.html', {
        'user_data': user_data
    })

Custom Template Filters for Security

# templatetags/security_filters.py
from django import template
from django.utils.safestring import mark_safe
from django.utils.html import escape
import bleach
import re

register = template.Library()

@register.filter
def sanitize_html(value):
    """Sanitize HTML content to prevent XSS"""
    if not value:
        return ''
    
    # Define allowed tags and attributes
    allowed_tags = [
        'p', 'br', 'strong', 'em', 'u', 'ol', 'ul', 'li',
        'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote',
        'a', 'img'
    ]
    
    allowed_attributes = {
        'a': ['href', 'title'],
        'img': ['src', 'alt', 'width', 'height'],
    }
    
    # Additional protocols for links
    allowed_protocols = ['http', 'https', 'mailto']
    
    # Clean the HTML
    cleaned = bleach.clean(
        value,
        tags=allowed_tags,
        attributes=allowed_attributes,
        protocols=allowed_protocols,
        strip=True
    )
    
    return mark_safe(cleaned)

@register.filter
def strip_scripts(value):
    """Remove all script tags and javascript: URLs"""
    if not value:
        return ''
    
    # Remove script tags
    value = re.sub(r'<script[^>]*>.*?</script>', '', value, flags=re.IGNORECASE | re.DOTALL)
    
    # Remove javascript: URLs
    value = re.sub(r'javascript:', '', value, flags=re.IGNORECASE)
    
    # Remove on* event handlers
    value = re.sub(r'\son\w+\s*=\s*["\'][^"\']*["\']', '', value, flags=re.IGNORECASE)
    
    return value

@register.filter
def safe_markdown(value):
    """Convert markdown to safe HTML"""
    if not value:
        return ''
    
    import markdown
    from markdown.extensions import codehilite, fenced_code
    
    # Convert markdown to HTML
    md = markdown.Markdown(
        extensions=['codehilite', 'fenced_code', 'tables'],
        extension_configs={
            'codehilite': {
                'css_class': 'highlight',
                'use_pygments': True,
            }
        }
    )
    
    html = md.convert(value)
    
    # Sanitize the resulting HTML
    return sanitize_html(html)

Using Security Filters in Templates

<!-- Load custom security filters -->
{% load security_filters %}

<div class="article-content">
    <!-- Sanitize user-generated HTML content -->
    {{ article.content|sanitize_html }}
</div>

<div class="user-comment">
    <!-- Convert markdown to safe HTML -->
    {{ comment.content|safe_markdown }}
</div>

<div class="user-bio">
    <!-- Strip potentially dangerous scripts -->
    {{ user.bio|strip_scripts|linebreaks }}
</div>

Input Validation and Sanitization

Form-Level Validation

# forms.py - Secure form validation
from django import forms
from django.core.exceptions import ValidationError
import bleach
import re

class ArticleForm(forms.ModelForm):
    """Secure article form with XSS prevention"""
    
    class Meta:
        model = Article
        fields = ['title', 'content', 'tags']
    
    def clean_title(self):
        """Validate and sanitize title"""
        title = self.cleaned_data['title']
        
        # Remove any HTML tags from title
        title = bleach.clean(title, tags=[], strip=True)
        
        # Check for suspicious patterns
        if re.search(r'<script|javascript:|on\w+\s*=', title, re.IGNORECASE):
            raise ValidationError("Title contains potentially dangerous content")
        
        return title
    
    def clean_content(self):
        """Validate and sanitize content"""
        content = self.cleaned_data['content']
        
        # Allow only safe HTML tags
        allowed_tags = [
            'p', 'br', 'strong', 'em', 'u', 'ol', 'ul', 'li',
            'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote',
            'a', 'img', 'code', 'pre'
        ]
        
        allowed_attributes = {
            'a': ['href', 'title'],
            'img': ['src', 'alt', 'width', 'height'],
        }
        
        # Sanitize HTML content
        content = bleach.clean(
            content,
            tags=allowed_tags,
            attributes=allowed_attributes,
            protocols=['http', 'https'],
            strip=True
        )
        
        return content

class CommentForm(forms.ModelForm):
    """Secure comment form"""
    
    class Meta:
        model = Comment
        fields = ['content']
        widgets = {
            'content': forms.Textarea(attrs={
                'placeholder': 'Write your comment... (HTML not allowed)',
                'rows': 4
            })
        }
    
    def clean_content(self):
        """Strip all HTML from comments"""
        content = self.cleaned_data['content']
        
        # Remove all HTML tags
        content = bleach.clean(content, tags=[], strip=True)
        
        # Additional validation
        if len(content.strip()) < 3:
            raise ValidationError("Comment must be at least 3 characters long")
        
        if len(content) > 1000:
            raise ValidationError("Comment cannot exceed 1000 characters")
        
        return content

Model-Level Validation

# models.py - Model validation for XSS prevention
from django.db import models
from django.core.exceptions import ValidationError
import bleach

def validate_no_scripts(value):
    """Validator to ensure no script tags in content"""
    if '<script' in value.lower() or 'javascript:' in value.lower():
        raise ValidationError("Script content is not allowed")

def validate_safe_html(value):
    """Validator for safe HTML content"""
    # Check if content contains only allowed tags
    allowed_tags = ['p', 'br', 'strong', 'em', 'u', 'a']
    cleaned = bleach.clean(value, tags=allowed_tags, strip=True)
    
    if cleaned != value:
        raise ValidationError("Content contains disallowed HTML tags")

class Article(models.Model):
    title = models.CharField(
        max_length=200,
        validators=[validate_no_scripts]
    )
    content = models.TextField(
        validators=[validate_safe_html]
    )
    author = models.ForeignKey(User, on_delete=models.CASCADE)
    
    def clean(self):
        """Additional model-level validation"""
        super().clean()
        
        # Sanitize content before saving
        if self.content:
            self.content = bleach.clean(
                self.content,
                tags=['p', 'br', 'strong', 'em', 'u', 'a'],
                attributes={'a': ['href']},
                strip=True
            )

class UserProfile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    bio = models.TextField(
        max_length=500,
        blank=True,
        help_text="Plain text only - HTML tags will be removed"
    )
    website = models.URLField(blank=True)
    
    def save(self, *args, **kwargs):
        """Override save to sanitize bio"""
        if self.bio:
            # Remove all HTML tags from bio
            self.bio = bleach.clean(self.bio, tags=[], strip=True)
        
        super().save(*args, **kwargs)

Content Security Policy (CSP)

Implementing CSP Headers

# middleware.py - CSP middleware
class ContentSecurityPolicyMiddleware:
    """Add Content Security Policy headers"""
    
    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 based on request"""
        
        # Base policy
        directives = [
            "default-src 'self'",
            "script-src 'self' 'unsafe-inline'",  # Allow inline scripts (be careful!)
            "style-src 'self' 'unsafe-inline'",   # Allow inline styles
            "img-src 'self' data: https:",        # Allow images from self, data URLs, and HTTPS
            "font-src 'self'",
            "connect-src 'self'",
            "frame-ancestors 'none'",             # Prevent clickjacking
            "base-uri 'self'",
            "form-action 'self'"
        ]
        
        # Adjust policy for admin pages
        if request.path.startswith('/admin/'):
            # Admin needs more permissive policy
            directives = [
                "default-src 'self'",
                "script-src 'self' 'unsafe-inline' 'unsafe-eval'",
                "style-src 'self' 'unsafe-inline'",
                "img-src 'self' data:",
                "font-src 'self'",
            ]
        
        # Development vs production policies
        if settings.DEBUG:
            # More permissive for development
            directives.append("script-src 'self' 'unsafe-inline' 'unsafe-eval' localhost:*")
        
        return '; '.join(directives)

# Alternative: Using django-csp package
# pip install django-csp

# settings.py
MIDDLEWARE = [
    'csp.middleware.CSPMiddleware',
    # ... other middleware
]

# CSP settings
CSP_DEFAULT_SRC = ("'self'",)
CSP_SCRIPT_SRC = ("'self'", "'unsafe-inline'")
CSP_STYLE_SRC = ("'self'", "'unsafe-inline'")
CSP_IMG_SRC = ("'self'", "data:", "https:")
CSP_FONT_SRC = ("'self'",)
CSP_CONNECT_SRC = ("'self'",)
CSP_FRAME_ANCESTORS = ("'none'",)
CSP_BASE_URI = ("'self'",)
CSP_FORM_ACTION = ("'self'",)

# Report violations (optional)
CSP_REPORT_URI = '/csp-report/'

CSP-Compatible JavaScript

<!-- Instead of inline scripts, use external files or nonces -->

<!-- BAD: Inline script (blocked by strict CSP) -->
<script>
    function handleClick() {
        alert('Clicked!');
    }
</script>

<!-- GOOD: External script file -->
<script src="{% static 'js/handlers.js' %}"></script>

<!-- GOOD: Using nonce for inline scripts -->
<script nonce="{{ csp_nonce }}">
    function handleClick() {
        alert('Clicked!');
    }
</script>

<!-- GOOD: Event handlers in JavaScript, not HTML -->
<!-- Instead of: <button onclick="handleClick()"> -->
<button id="my-button">Click me</button>
<script>
    document.getElementById('my-button').addEventListener('click', handleClick);
</script>

Advanced XSS Prevention

Rich Text Editor Security

# views.py - Secure rich text handling
from django.utils.html import strip_tags
import bleach

class SecureRichTextMixin:
    """Mixin for secure rich text handling"""
    
    allowed_tags = [
        'p', 'br', 'strong', 'em', 'u', 'ol', 'ul', 'li',
        'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote',
        'a', 'img', 'table', 'thead', 'tbody', 'tr', 'td', 'th'
    ]
    
    allowed_attributes = {
        'a': ['href', 'title'],
        'img': ['src', 'alt', 'width', 'height'],
        'table': ['class'],
        'td': ['colspan', 'rowspan'],
        'th': ['colspan', 'rowspan'],
    }
    
    def sanitize_rich_text(self, content):
        """Sanitize rich text content"""
        if not content:
            return ''
        
        # Clean HTML with bleach
        cleaned = bleach.clean(
            content,
            tags=self.allowed_tags,
            attributes=self.allowed_attributes,
            protocols=['http', 'https', 'mailto'],
            strip=True
        )
        
        # Additional custom sanitization
        cleaned = self.remove_dangerous_attributes(cleaned)
        
        return cleaned
    
    def remove_dangerous_attributes(self, html):
        """Remove potentially dangerous attributes"""
        import re
        
        # Remove style attributes (can contain JavaScript)
        html = re.sub(r'\sstyle\s*=\s*["\'][^"\']*["\']', '', html, flags=re.IGNORECASE)
        
        # Remove data attributes (can be used for attacks)
        html = re.sub(r'\sdata-\w+\s*=\s*["\'][^"\']*["\']', '', html, flags=re.IGNORECASE)
        
        return html

class ArticleCreateView(SecureRichTextMixin, CreateView):
    """Secure article creation with rich text"""
    model = Article
    form_class = ArticleForm
    
    def form_valid(self, form):
        """Sanitize content before saving"""
        form.instance.content = self.sanitize_rich_text(form.instance.content)
        return super().form_valid(form)

File Upload Security

# File upload security to prevent XSS via uploaded files
import magic
from django.core.exceptions import ValidationError

def validate_file_content(file):
    """Validate uploaded file content"""
    
    # Check file type using python-magic
    file_type = magic.from_buffer(file.read(1024), mime=True)
    file.seek(0)  # Reset file pointer
    
    allowed_types = [
        'image/jpeg', 'image/png', 'image/gif',
        'application/pdf', 'text/plain'
    ]
    
    if file_type not in allowed_types:
        raise ValidationError(f"File type {file_type} not allowed")
    
    # Additional checks for image files
    if file_type.startswith('image/'):
        validate_image_content(file)

def validate_image_content(file):
    """Additional validation for image files"""
    
    # Check for embedded scripts in image metadata
    file.seek(0)
    content = file.read()
    
    # Look for script tags in image data (SVG attacks)
    if b'<script' in content.lower() or b'javascript:' in content.lower():
        raise ValidationError("Image contains potentially malicious content")
    
    file.seek(0)

class SecureFileField(models.FileField):
    """Secure file field with content validation"""
    
    def __init__(self, *args, **kwargs):
        kwargs.setdefault('validators', []).append(validate_file_content)
        super().__init__(*args, **kwargs)

# Usage in models
class Document(models.Model):
    title = models.CharField(max_length=200)
    file = SecureFileField(upload_to='documents/')
    uploaded_by = models.ForeignKey(User, on_delete=models.CASCADE)

Testing XSS Protection

XSS Security Tests

# tests.py - Testing XSS protection
from django.test import TestCase, Client
from django.contrib.auth.models import User
from django.urls import reverse

class XSSProtectionTests(TestCase):
    """Test XSS protection mechanisms"""
    
    def setUp(self):
        self.client = Client()
        self.user = User.objects.create_user(
            username='testuser',
            password='testpass123'
        )
    
    def test_template_auto_escaping(self):
        """Test that templates automatically escape user input"""
        
        # Create article with potentially malicious content
        malicious_title = '<script>alert("XSS")</script>'
        
        article = Article.objects.create(
            title=malicious_title,
            content='Test content',
            author=self.user
        )
        
        response = self.client.get(reverse('article_detail', args=[article.id]))
        
        # Check that script tags are escaped
        self.assertContains(response, '&lt;script&gt;')
        self.assertNotContains(response, '<script>')
    
    def test_form_input_sanitization(self):
        """Test that form input is properly sanitized"""
        
        self.client.login(username='testuser', password='testpass123')
        
        # Submit form with malicious content
        response = self.client.post(reverse('create_article'), {
            'title': '<script>alert("XSS")</script>',
            'content': '<p>Safe content</p><script>alert("XSS")</script>'
        })
        
        # Check that article was created with sanitized content
        article = Article.objects.latest('id')
        self.assertNotIn('<script>', article.title)
        self.assertNotIn('<script>', article.content)
    
    def test_json_script_safety(self):
        """Test that json_script filter is safe"""
        
        # Data with potentially dangerous content
        user_data = {
            'name': '</script><script>alert("XSS")</script>',
            'bio': '<img src=x onerror=alert("XSS")>'
        }
        
        response = self.client.get(reverse('profile'), {
            'user_data': user_data
        })
        
        # Check that dangerous content is properly escaped in JSON
        self.assertNotContains(response, '<script>alert("XSS")</script>')
    
    def test_csp_headers(self):
        """Test that CSP headers are present"""
        
        response = self.client.get('/')
        
        self.assertIn('Content-Security-Policy', response)
        csp = response['Content-Security-Policy']
        self.assertIn("default-src 'self'", csp)
        self.assertIn("script-src", csp)

Best Practices Summary

Template Security

  • Never use |safe filter with user input
  • Use json_script for passing data to JavaScript
  • Implement custom template filters for HTML sanitization
  • Use CSP headers to prevent inline script execution

Input Validation

  • Sanitize all user input at multiple levels
  • Use whitelist approach for allowed HTML tags
  • Validate file uploads for malicious content
  • Implement proper form validation

Output Encoding

  • Rely on Django's automatic escaping
  • Use appropriate filters for different contexts
  • Escape data properly when using raw SQL or APIs
  • Be careful with JavaScript context escaping

Content Security Policy

  • Implement strict CSP headers
  • Avoid inline scripts and styles
  • Use nonces or hashes for necessary inline content
  • Monitor CSP violation reports

Next Steps

Now that you understand XSS prevention, let's explore SQL injection protection and how Django's ORM helps prevent database attacks.