Django XSS Protection Disabled
ID |
python.django_xss_protection_disabled |
Severity |
high |
Remediation Complexity |
trivial |
Remediation Risk |
low |
Remediation Effort |
low |
Resource |
Misconfiguration |
Language |
Python |
Tags |
ASVS50:v1.3.1, CWE:80, NIST.SP.800-53, OWASP:2025:A02, OWASP:2025:A05, PCI-DSS:6.5.7, django |
Description
Improper configurations or misuse of Django’s templating system can disable XSS protection, exposing applications to cross-site scripting attacks.
Rationale
Cross-Site Scripting (XSS) is a vulnerability that occurs when an application includes untrusted data in a web page without proper validation or escaping.
Django’s templating system is designed with built-in protections to prevent cross-site scripting (XSS) vulnerabilities. These protections automatically escape HTML input, ensuring that user-supplied content is not executed as code on the client side.
However, developers may inadvertently disable these protections by marking content as "safe" or bypassing the template engine, increasing the risk of XSS attacks. Some common misconfigurations and practices can lead to such vulnerabilities:
Global Autoescape disabled Setting
Global Autoescape is a feature in Django’s template system that automatically escapes variables when rendering templates. This crucial security measure helps prevent Cross-Site Scripting (XSS) attacks by ensuring that any HTML, JavaScript, or CSS code in user inputs is not executed by the browser.
# Example of disabling autoescape in template settings
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'OPTIONS': {'autoescape': False}, # FLAW
},
]
Non Global Autoescape Disabled Settings
| These issues are reported with a lower severity degree. |
mark_safe
mark_safe is a utility function in Django that marks a string as safe for inclusion in HTML output without escaping. It’s part of Django’s template system and is used to inject existing HTML or JavaScript code into templates.
If user input is marked safe without proper sanitization, it can lead to cross-site scripting attacks by injecting malicious scripts.
from django.utils.safestring import mark_safe
html_content = "<strong>Important:</strong> Read this!"
safe_content = mark_safe(html_content) # FLAW
SafeString
SafeString is a class used internally by Django to indicate that a string is safe for HTML output, meaning it should not be auto-escaped by the template engine. It’s how Django keeps track of which strings have been marked as safe (When you use mark_safe, it returns a SafeString instance).
If user input is marked safe without proper sanitization, it can lead to cross-site scripting attacks by injecting malicious scripts.
from django.utils.safestring import SafeString
html_content = "<strong>Important:</strong> Read this!"
safe_content = SafeString(html_content) # FLAW
What is not reported
mark_safe and SafeString over text the author wrote suppress no escaping decision, so they are not
reported. That covers a literal, a translated literal, and any concatenation or format of those:
mark_safe('<code>100,200,300</code>') # not reported
mark_safe(_('Assigned tagged VLAN IDs, encased with double quotes')) # not reported
mark_safe(_('Example:') + ' <code>"100,200,300"</code>') # not reported
It also covers an expression that escapes every value it interpolates. mark_safe is how Django returns
pre-escaped HTML, so marking an f-string or a format whose dynamic parts all pass through escape,
conditional_escape, escapejs or format_html suppresses no escaping decision either:
mark_safe(f'<span class="badge text-bg-{escape(color)}">{escape(label)}</span>') # not reported
mark_safe(_('Must match: <code>{regex}</code>').format(regex=escape(pattern))) # not reported
Where escaping is not what makes it safe
The exemption above is refused wherever HTML escaping is not the protection that matters. escape()
turns < into <; it leaves javascript:alert(1) untouched, so an escaped value that opens an
href, src, action or style, or that lands inside a <script> element or an event-handler
attribute, is still reported:
mark_safe(f'<a href="{escape(value)}">{escape(value)}</a>') # reported: value can be a javascript: URI
In URL position only an application-generated URL passes — a reverse()`d route, `resolve_url,
static, or Django’s get_absolute_url convention:
mark_safe(' / '.join(f'<a href="{n.get_absolute_url()}">{escape(n)}</a>' for n in nodes)) # not reported
Values that carry no text of anyone’s choosing
A model’s own field read through self in one of its methods passes when the model, or a model base it
inherits, declares it a number, a boolean, a date, a duration or a UUID. A field with choices, a
SlugField, or one an anchored RegexValidator (or validate_slug) restricts passes where its characters
cannot be misused — no : or & in a URL, no <, > or quote in markup — as long as no ORM write of
the project (objects.create(f=…), .update(f=…), get_or_create, update_or_create, a model built in
bulk_create) hands that field a value that is not a literal or a form’s cleaned_data. The url of a
FileField or ImageField passes, unless a storage class of the project overrides url().
A read of the settings or of the configuration administrators edit in the database is the operators' text
and passes too, as far as the rule’s properties trust it. Configuration is told by where the module imports it from, never by a name alone: the settings module
(from django.conf import settings); the configuration libraries administrators edit — config from
django-constance, global_preferences_registry.manager() from django-dynamic-preferences,
get_solo() on a django-solo SingletonModel, for_request()/for_site()/load() on a Wagtail
BaseSiteSetting or BaseGenericSetting; a function of the project every return of which is one of
those reads; and the accessors the trusted_config_accessors property lists. A project object or function
that merely has a configuration-like name (config, get_config()) is not configuration. A value fetched from somewhere
else — a feed, a remote catalogue — is not configuration either:
class Product(models.Model):
code = models.CharField(validators=[RegexValidator(r'^[A-Z0-9-]+$')])
def code_tag(self):
return mark_safe(f'<code>{self.code}</code>') # not reported
mark_safe(settings.BANNER_HTML) # not reported (trust_settings)
mark_safe(config.BANNER_TOP) # not reported (constance, trust_admin_config)
mark_safe(SiteConfiguration.get_solo().footer) # not reported (django-solo, trust_admin_config)
mark_safe(get_config().BANNER_TOP) # reported, unless trusted_config_accessors lists get_config
mark_safe(requests.get(CATALOGUE).json()['summary']) # reported
What is still reported
A module-level constant is resolved: a name the file assigns exactly once outside every function and class. A name assigned twice, or imported rather than assigned, resolves to nothing and the call is reported.
A local name is resolved when every binding of it in the function holds safe HTML, wherever the
binding sits: a plain assignment, a +=, and an .append() on a list the function creates. A
parameter passes only when the statement right before the use rebinds it to a safe value:
PLACEHOLDER_HTML = '<span class="text-muted">—</span>'
mark_safe(PLACEHOLDER_HTML) # not reported: assigned once, at module level
badges = []
for label, color in parts:
badges.append(f'<span class="badge text-bg-{escape(color)}">{escape(label)}</span>')
mark_safe(' '.join(badges)) # not reported: every element appended is escaped
value = conditional_escape(value)
mark_safe(f'<pre>{value}</pre>') # not reported: escaped right before
Any binding that cannot be read that way leaves the name unresolved, and the call is reported: a
parameter, a value appended raw on any branch, an .extend(), an augmented operator other than +=, a
subscript assignment or tuple unpacking, a loop, with or except target, a list handed to another
call, and a global or nonlocal name.
mark_safe(html) # reported: html is a parameter
The same treatment applies to Markup in python.flask_xss_protection_disabled.
Custom Filter
In Django, custom template filters can be created to manipulate data in templates. By default, Django auto-escapes filter output to prevent XSS vulnerabilities. However, you can mark a custom filter’s output as safe by setting is_safe=True.
If user input is marked safe without proper sanitization, it can lead to cross-site scripting attacks by injecting malicious scripts.
from django import template
register = template.Library()
@register.filter(is_safe=True) # FLAW
def highlight(value):
return f"<strong>{value}</strong>"
HTML Magic
The html magic method in Django is used to define how an object should be rendered as HTML. If this method is defined, Django’s template engine will use its return value during HTML rendering, and it will not be escaped.
If user input is marked safe without proper sanitization, it can lead to cross-site scripting attacks by injecting malicious scripts.
class SafeContent:
def __init__(self, content):
self.content = content
def __html__(self): # FLAW
return f"<strong>{self.content}</strong>"
obj = SafeContent("Hello, World!")
html_output = obj.__html__()
html_safe
The html_safe decorator in Django is used to automatically add an html method to a class. This method indicates that the class’s string representation should be treated as safe HTML, exempting it from auto-escaping in templates.
If user input is marked safe without proper sanitization, it can lead to cross-site scripting attacks by injecting malicious scripts.
from django.utils.decorators import html_safe
@html_safe # FLAW
class HtmlContent(str):
pass
content = HtmlContent("<em>Safe HTML Content</em>")
Configuration
| Property | Default | Meaning |
|---|---|---|
|
|
A read of the Django settings module is safe in a URL and in markup. Set to |
|
|
A read of the configuration administrators edit in the database ( |
|
|
Fully qualified names of further functions or objects that return configuration, such as |
Remediation
Carefully review every issue addressed by this detector to ensure that the unescaped HTML code is safe.
To effectively address XSS vulnerabilities when using Django enable all the autoescape settings.
References
-
CWE-80 : Improper Neutralization of Script-Related HTML Tags in a Web Page (Basic XSS).