Vulnerability
Django

XSS in Django Applications

Django's template engine auto-escapes HTML by default, making it one of the more secure frameworks against XSS. However, the |safe filter, mark_safe(), and {% autoescape off %} provide explicit escape hatches that developers frequently misuse with user-controlled data.

Scan Your Django App

How XSS Manifests in Django

In Django, XSS occurs when developers explicitly disable auto-escaping for user content. The |safe template filter is the most common vector: {{ user_bio|safe }} renders raw HTML. The mark_safe() function in Python views has the same effect. The {% autoescape off %} block disables escaping for entire template sections. Developers sometimes use this when rendering CMS content or email templates, inadvertently exposing user-controlled fields. JavaScript context injection is another vector: embedding Django template variables inside <script> tags requires JSON serialization, not HTML escaping. Using {{ value }} inside a script tag can lead to script injection even with auto-escaping enabled. Custom template tags that use mark_safe() on user input also create XSS vulnerabilities.

Real-World Impact

A Django CMS platform used the |safe filter to render page content. An editor account was compromised, and the attacker injected a cryptocurrency mining script into a popular page. The script ran in every visitor's browser, consuming CPU resources and generating revenue for the attacker while degrading user experience. Another Django application embedded user data in a JavaScript variable using {{ value }} inside a script tag. An attacker closed the string context and injected arbitrary JavaScript that stole CSRF tokens and performed unauthorized actions.

Step-by-Step Fix

1

Replace |safe with sanitized output

Use the bleach library to sanitize HTML before marking it as safe in templates.

# views.py
import bleach
from django.utils.safestring import mark_safe

ALLOWED_TAGS = ['b', 'i', 'em', 'strong', 'a', 'p', 'br', 'ul', 'ol', 'li']
ALLOWED_ATTRS = {'a': ['href', 'title', 'target', 'rel']}

def user_profile(request, user_id):
    user = get_object_or_404(User, pk=user_id)
    clean_bio = bleach.clean(
        user.bio,
        tags=ALLOWED_TAGS,
        attributes=ALLOWED_ATTRS,
        strip=True
    )
    return render(request, 'profile.html', {
        'bio': mark_safe(clean_bio)
    })
2

Use json_script for JavaScript context

The |json_script filter safely embeds Python data in a script tag.

<!-- UNSAFE: XSS in JavaScript context -->
<script>
  var userName = "{{ user.name }}";
</script>

<!-- SAFE: json_script creates a <script type="application/json"> tag -->
{{ user_data|json_script:"user-data" }}
<script>
  const userData = JSON.parse(
    document.getElementById('user-data').textContent
  );
</script>
3

Add CSP middleware

Use django-csp to add Content Security Policy headers.

# settings.py
INSTALLED_APPS = [
    ...
    'csp',
]

MIDDLEWARE = [
    'csp.middleware.CSPMiddleware',
    ...
]

CSP_DEFAULT_SRC = ("'self'",)
CSP_SCRIPT_SRC = ("'self'",)
CSP_STYLE_SRC = ("'self'", "'unsafe-inline'")
CSP_IMG_SRC = ("'self'", "data:")
4

Audit template tags

Review all custom template tags for unsafe HTML rendering.

# UNSAFE custom template tag
from django import template
from django.utils.safestring import mark_safe

register = template.Library()

@register.simple_tag
def render_comment(comment):
    return mark_safe(f'<div class="comment">{comment.body}</div>')

# SAFE version
import bleach

@register.simple_tag
def render_comment(comment):
    clean = bleach.clean(comment.body)
    return mark_safe(f'<div class="comment">{clean}</div>')

Prevention Best Practices

1. Never use |safe, mark_safe(), or {% autoescape off %} with user-controlled data. 2. When embedding data in JavaScript, use the |json_script filter instead of {{ value }} inside script tags. 3. If you must render user HTML, use the bleach library to sanitize it before marking as safe. 4. Add Content Security Policy headers via Django middleware. 5. Review custom template tags for mark_safe() usage with untrusted data.

How to Test

1. Search your templates for |safe, {% autoescape off %}, and mark_safe usage. 2. Test each instance by submitting <script>alert(1)</script> through the corresponding input. 3. Check for {{ variable }} usage inside <script> tags. 4. Review custom template tags in your templatetags/ directories. 5. Use Vibe App Scanner to automatically detect XSS patterns in your Django application.

Frequently Asked Questions

Does Django automatically prevent XSS?

Yes, Django's template engine auto-escapes HTML in {{ variable }} expressions by default. However, the |safe filter, mark_safe(), and {% autoescape off %} explicitly disable this protection. Django also does not protect against XSS in JavaScript contexts - you need the |json_script filter for that.

What is the difference between |safe and mark_safe in Django?

Both disable HTML escaping for the output. |safe is used in templates to mark a variable as safe HTML. mark_safe() is used in Python code (views, template tags) to mark a string as safe. Both are dangerous when used with user-controlled data and should always be paired with sanitization.

Is bleach the best sanitization library for Django?

Bleach was the go-to library but is now in maintenance mode. For new projects, consider nh3 (a Rust-based sanitizer with Python bindings) which is faster and actively maintained. Both integrate well with Django and can be used with mark_safe() after sanitization.

Is Your Django App Vulnerable to XSS?

VAS automatically scans for xss vulnerabilities in Django applications and provides step-by-step remediation guidance with code examples.

Scans from $5, results in minutes. Get actionable fixes tailored to your Django stack.