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.
<!-- 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>
# 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 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: <script>alert('XSS')</script> -->
<!-- SAFE: Default escaping -->
<div class="title">{{ article.title }}</div>
<!-- Input: <script>alert('XSS')</script> -->
<!-- Output: <script>alert('XSS')</script> -->
<!-- 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! -->
<!-- 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 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
})
# 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)
<!-- 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>
# 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
# 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)
# 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/'
<!-- 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>
# 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 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)
# 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, '<script>')
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)
|safe filter with user inputjson_script for passing data to JavaScriptNow that you understand XSS prevention, let's explore SQL injection protection and how Django's ORM helps prevent database attacks.
Cross Site Request Forgery
Cross-Site Request Forgery (CSRF) is an attack that forces an end user to execute unwanted actions on a web application in which they're currently authenticated. Django provides robust built-in CSRF protection that's enabled by default.
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.