Template XSS Protection Disabled

ID

python.template_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

Description

Improper configurations or misuse of Python’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.

Python’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:

Template Variables In Dangerous Locations

Unquoted Variable In HTML Attribute

When an HTML attribute value is unquoted, it can lead to parsing issues or security vulnerabilities such as injection attacks. Quoting attribute values is a best practice to ensure integrity and security.

See the following template example:

<input type=text name=username value={{ username }}>

If username includes spaces or special characters, it may break the HTML structure or lead to unexpected behavior.

Variable In Href Attribute

When using template variables in href attributes, there’s a risk of allowing JavaScript URIs, which can lead to Cross-Site Scripting (XSS) vulnerabilities. HTML escaping doesn’t mitigate this risk because it doesn’t validate the URI scheme itself.

See the following template example:

<a href="{{ user_input }}">Click here</a>

If user_input contains "javascript:alert('XSS')" it could execute a script.

Because a javascript: URI is the whole of the residual risk, the variable is only reported when it can supply the URL’s scheme — that is, when it opens the attribute value. A variable that lands further in, after a {% url %} tag, a literal path, a ? or a #, cannot introduce one: the scheme is already decided, and the engine’s own escaping turns " into " so the value cannot break out of the attribute either.

<a href="{{ entry.link }}">Feed item</a>                                    <!-- reported -->
<a href="{% url 'core:list' %}?id={{ value }}">Filtered</a>                 <!-- not reported -->
<a href="/static/docs/{{ page }}">Docs</a>                                  <!-- not reported -->

Even in scheme position, an expression the application generates cannot carry one, so it is not reported: get_absolute_url (Django’s convention for a model’s canonical path, a reverse()`d route), `request.path, and a primary key or _id, which is an integer.

<a href="{{ object.get_absolute_url }}">Object</a>                          <!-- not reported -->
<a href="{{ request.path }}">Reload</a>                                     <!-- not reported -->

Variable In Script Block

When placing template variables directly into a JavaScript block, there’s a risk of code injection, which can lead to XSS vulnerabilities. HTML escaping doesn’t protect against this risk because JavaScript can execute without relying on special HTML characters.

<script>
    var userInput = {{ user_input }};
</script>

If user_input contains malicious code like alert('XSS');, it can execute in the context of your page.

Template Unescaped Variables

These issues are reported with a lower severity degree.

Safe and Safeseq Filters

These filters mark a string or sequence as safe, preventing it from being auto-escaped in a Django template.

<p>{{ user_inputs|safeseq|join:", " }}</p>

A plain {{ value|safe }} in a template file is not reported when every value the views render it with is safe as markup: the author’s text, or configuration the rule trusts (see Configuration).

Values the application’s models and configuration hold

A variable is also settled by what holds its value. On a model instance a view renders — X.objects.get(…​), X.objects.filter(…​).first(), get_object_or_404(X, …​), an element of X.objects.filter(…​) — a field the model declares a number, a date or a UUID, a validated slug, and the url of a FileField are not reported in an href. A read of the settings or of the configuration administrators edit is safe in an href and under |safe. 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 a remote feed or catalogue is still reported:

def support(request):
    entry = requests.get(settings.CATALOGUE_URL).json()
    return render(request, 'support.html', {
        'support_url': settings.SUPPORT_URL,     # not reported (trust_settings)
        'banner': config.BANNER_TOP,             # not reported under |safe (constance, trust_admin_config)
        'homepage': entry['homepage'],           # reported
    })

Autoescape Off Block

Using the {% autoescape off %} block within your templates disables the automatic HTML escaping feature, allowing content to be rendered as raw HTML. This can be useful for certain scenarios but poses significant security risks if not handled carefully.

{% autoescape off %}
{{ user_input }}
{% endautoescape %}

In this example, any HTML in user_input would be rendered directly without escaping.

Configuration

Property Default Meaning

trust_settings

true

A value read from the Django settings module is safe in an href and under |safe. Set to false to report it like any other value.

trust_admin_config

true

A value read from the configuration administrators edit in the database (django-constance, the global preferences of django-dynamic-preferences, a django-solo singleton, Wagtail settings) is safe in an href and under |safe. Set to false to report it like any other value.

trusted_config_accessors

[]

Fully qualified names of further functions or objects that return configuration, such as myapp.config.get_config. A value from a call to a listed function, or a read of a listed object, that the import of the module resolves to that name is safe in an href and under |safe, whatever the trust properties say. A bare name is ignored.

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 Python templates enable all the autoescape settings.

References

© 2026 Xygeni. All rights reserved