Preventing XSS in Django
2023年3月13日
0 分で読めますCross-Site Scripting (XSS) is a type of vulnerability that involves manipulating user interaction with a web application to compromise a user's browser environment. These vulnerabilities can affect many web apps, including those built with modern frameworks such as Django.
Since XSS attacks are so prevalent, it's essential to safeguard your applications against them. This guide discusses how XSS vulnerabilities originate in Django apps and what you can do to mitigate them. You'll also learn how to use free security tools to detect and fix XSS vulnerabilities early in development.
What is XSS?
The term XSS comes from the early days of these attacks when stealing cross-site data was an attacker’s main focus. But XSS attacks have evolved and are now considered to be any attack that allows attackers to compromise client-side data/tokens/etc. Successful attacks can lead to anything from session hijacking to complete account or system takeover.
In an XSS attack, unwanted data enters an application and is rendered back to a user without validation. The malicious data sent as part of the response often takes the form of JavaScript or other code that can run in the user's browser. When the browser executes this code, it bypasses its same-origin policy (SOP) and exploits the user.
Historically, XSS attacks have been categorized into two types based on data persistence — reflective and stored. There's also a newer type of XSS attack called DOM-based XSS.
Stored XSS
Stored XSS attacks, sometimes called persistent XSS attacks, happen when malicious code is stored in a vulnerable application's server and is later sent to users without sanitation (and eventually executed by a user’s browser). The malicious payload is often stored in databases, forum posts, logs, or comment fields.
When a victim visits the affected application, the malicious payload is rendered to the victim's browser as part of the server response. The browser then executes this data, and the user is compromised.
Reflected XSS
Reflected XSS attacks happen when user input returned by the application is not saved by the server. In this case, the untrusted code is sent as part of a search result or error message without being properly sanitized.
The attacker usually sends the payload to the victim in disguise. When the victim clicks on the payload, it sends the malicious code to the affected application. The application responds with the malicious code, which executes in the victim's browser.
DOM-based XSS
DOM-based XSS attacks happen when the malicious data flow begins and ends in the browser DOM. The vulnerability is triggered by manipulating the victim browser's DOM; the resulting HTTP response doesn't change itself, but simply returns the client-side code, which then modifies the browser DOM in a way that runs the payload.
Preventing XSS in Django
Django offers built-in protections against XSS attacks by including an auto-escape mechanism for its templating engine. However, although it can stop common XSS attacks, it can't prevent attack vectors arising from bad coding practices.
Vulnerability to XSS attacks can be minimized by ensuring that you follow best practices when building your app.
Quote dynamic data
Any part of your app that accepts user input without sanitization can introduce XSS vulnerabilities. Attackers can use these input fields to inject arbitrary JavaScript code that does not require HTML characters.
For example, the following HTML uses an unquoted attribute that can be modified to contain JavaScript handlers, such as onmouseover=alert(1)
:
1<div class={{ name }}></div>
Of course, real adversaries won't be so harmless. Instead, they'll inject custom payloads to make the most of this opportunity. Another example of XSS vulnerabilities stemming from unquoted payloads is using JavaScript variables to store user data:
1var user = new userClass();
2user.age = {{ age }} ;
Using JavaScript integers to hold a supplied value this way exposes your app to XSS attacks. Django's auto-escaping mechanism will not protect against this. An attacker can enter something as trivial as 23 ; payload()
and execute malicious code in a user’s browser.
Quoting all user-facing attributes with double quotes will mitigate these vulnerabilities:
1<div class="{{ name }}"></div>
Avoid template literals
If your code contains JavaScript template literals, it might be vulnerable to XSS. Template literals use back-ticks instead of quotes, and do not escape the characters:
1<img src="javascript:alert(`xss`)>
It allows attackers to execute code successfully by crafting payloads with back-ticks. The best way to get around this is to ban the use of template literals.
Validate attribute URLs
Many HTML attributes, such as a href
and img src
, expect a URL, which can be exploited for XSS attacks. Attackers can simply put payloads with a javascript:
URI to execute their code. Django's HTML escaping will not protect you from this case.
1<a href="{{ user.website_url }}">Website</a>
For example, say your app lets users add their personal website's URL. Malicious users can use something like javascript:payload(XSS)
as their link. Now whenever someone clicks this link, it'll execute the malicious payload.
To solve this, you can create a safelist of allowed protocols for URLs and ban everything else. Some safe URIs include http:
, https:
, ftp:
, and mailto:
. You may also sanitize attribute URLs before storing them in the database.
Escape JavaScript data
Placing variable data directly into JavaScript puts it into the execution context. If your program allows users to control data that's inserted into JavaScript blocks or event handlers, it can lead to potential XSS attacks.
1<script>var name = {{ username }};</script>
Since attackers can devise code without HTML characters, relying on Django's HTML escaping will not prevent XSS in such cases.
To mitigate this, you may ban template variables inside <script>
blocks, or use the json_script
template tag for reading JS data:
1{{ name|json_script:"username" }}
2<script>
3 var name = JSON.parse(document.getElementById(‘username’).textContent);
4</script>
Don't forget to encode data characters using the \xHH
format and verify that the Content-Type header for JSON is application/json
, not text/html
.
Escape CSS data
Data inserted into CSS-style tags or attributes can introduce XSS vulnerabilities in your Django app. Attackers can leverage this CSS data to get into the execution context. This is usually done by injecting JavaScript code into CSS contexts:
1<style> body{ margin-left:expression('alert(‘XSS’)') } </style>
Using the expression()
directive allows for arbitrary JavaScript statements to be used to evaluate a CSS property's value and will open up your Django app to XSS attacks.
To mitigate this, avoid setting your CSS parameter’s values using the expression()
directive. When using JavaScript statements to set or change CSS properties, try something like style.property = x
.
Minimal use of safe
Django's auto-escaping is good in most cases, but it can't help if you disable HTML escaping via the safe
filter inside a template. When you mark something safe
, Django doesn't escape it and renders the data as-is.
1<span id="query" >Searches similar to {{ query | safe }}</span>
If your app renders this query without validating the data, attackers could use it to feed payloads. Only use the safe filter when you're confident that the data is actually safe; user-supplied data should never be marked as safe
.
Restrict use of the safeseq filter
The safeseq
filter indicates that the content to be rendered is perfectly safe for use. It can introduce XSS payloads the same way that safe
can.
1{{ ids | safeseq | join:", " }}
Avoid using this filter for sequence data. If you must, use mark_safe()
. This will allow you to comb through instances where you've explicitly marked something safe during code review.
Limit using html_safe()
The html_safe()
method adds the __html__
magic method to the provided class, which returns an exact string representation of that class:
1@html_safe
2class UserData(str):
3 pass
This data is not escaped by Django's template engine and is therefore a potential XSS problem. html_safe()
shouldn't be used unless necessary. You should also avoid using the __html__
magic method inside any class.
Avoid filters with is_safe=True
If you register a custom filter with is_safe=True
, Django will not escape the HTML and will mark the returned value from the filter as safe.
1@register.filter(is_safe=True)
2def randomfilter(value):
3 return value
Since the return data from this filter doesn't go through Django's auto-escaping, it may lead to XSS attacks. To help ensure your application remains secure, refrain from registering filters this way unless you're sure the returned data is safe to use.
Limit mark_safe() and SafeString
The mark_safe()
method makes the rendered data safe and escapes Django's built-in XSS protections. If you use this method frequently, it can become a breeding ground for XSS vulnerabilities.
1mark_safe(some_content)
Moreover, when you use mark_safe()
, it returns the data as a SafeString. Django uses the SafeString
class to determine which data is safe to render and which is not. So what happens when you use SafeString
directly?
1SafeString(f"<div>{request.POST.get('id')}</div>")
As you may have guessed, using the SafeString
class directly in this way bypasses Django's HTML escaping and could lead to XSS. It's best to limit the use of both mark_safe()
and SafeString
.
Avoid writing responses using HttpResponse
Using HttpResponse
or similar classes directly in your code bypasses Django's template system, thus disabling HTML escaping.
1return HttpResponse("My name is, " + name)
This can open your program to XSS attacks, so writing responses this way should be avoided. Instead, you can use the render()
method with a template.
Detect and fix Django XSS vulnerabilities with Snyk
XSS attacks are one of the most active threats against modern web apps. Because they can occur in many ways, detecting all XSS vulnerabilities during code review is challenging. This makes it crucial to focus on catching these vulnerabilities early in the software development lifecycle (SDLC).
You can do so by integrating dedicated testing tools into your development environment. We'll use Snyk to demonstrate this.
Snyk offers several ways to test Django apps, including a Web UI, command line interface, IDE plug-ins, and APIs. In this tutorial, you'll use the Snyk PyCharm plug-in, and you'll need PyCharm installed to follow along (but Snyk also supports other IDEs, such as Eclipse and Visual Studio).
To use Snyk, create a free account, then install the plug-in by following the installation guide.
After installing the plugin, configure the Snyk plug-in for your IDE. You need to connect the plug-in to the Snyk platform, which requires the Snyk CLI. However, you don't need to install it separately. When you run a scan for the first time, Snyk will automatically download it.
You'll be prompted to authenticate Snyk. Click Test code now, and you’ll be taken to the Snyk Web UI.
Click Authenticate to confirm your plug-in integration.
After authentication is complete, Snyk automatically starts analyzing your Django app and raises issues under the Snyk tab in your IDE. Alternatively, you can trigger an analysis by clicking Run scan.
Snyk displays helpful information about the detected vulnerabilities, such as their severity ratings and suggested fixes.
You can fix the vulnerabilities by following the upgrades recommended by Snyk. The plug-in will always recommend the minimum changes required to fix your code.
Conclusion
XSS attacks are one of the most common security issues found in Django applications. Since they can arise from many sources and have different forms, it's hard to prevent them altogether. However, you can reduce your risk of XSS occurrences by following these best practices.
Using the right tools during development is also crucial. A dedicated security platform like Snyk can help you find and fix XSS vulnerabilities early in your app's development.