Secrets of Security in a Django application đĄď¸
Security is like that unsung hero in apps â super common and crazy important, but we tend to overlook it way too often. In this post, weâll dive into the thrilling world of web security. In this thrilling journey, weâll uncover the most common vulnerabilities in Django apps and arm you with elegant strategies to prevent and tackle them within Django. From securing your code to mastering essential protocols, get ready to defend your digital world like a pro.
Letâs dive into a few typical security vulnerabilities below
- SQL Injection
- CRLF Injection
- Timing Attack
- Clickjacking Attack
- Cross-Site Scripting (XSS)
- Cross-Site Request Forgery (CSRF)
- HTTP Strict Transport Security (SSL)
- Session Hijacking
- Denial of Service (DoS)
- Miscellaneous
1. SQL Injection
SQL injection is a type of attack where a malicious user can execute arbitrary SQL code on a database. This can result in records being deleted/updated for malicious purposes.
Django provides a way to write raw or custom SQL queries along with the ORM. However, using raw SQL queries can make your web application vulnerable to SQL injection. Consider below example â
from django.db import connection
cursor = connection.cursor()
username = request.GET['username']
sql_query = "SELECT * FROM users WHERE username = '%s';" % username
cursor.execute(sql_query)
At first glance, it seems a simple query that going to retrieve the user object from the table but can you guess what is going to happen if an attacker passes the below string in the username parameter?
'; UPDATE users SET is_superuser = true WHERE username = 'hacker
Yes, you are correct, itâs going to give the superuser permission to the user âhackerâ, and with the superuser permission, you know what an attacker can do.
Even worse, what if the attacker passes below value?
'; DROP TABLE users;'
To prevent this, always use Djangoâs inbuilt ORM for DB querying. If, for some reason, you canât do that, always make sure you validate/sanitize user-controlled data and pass it as a parameter instead of doing string formatting. Also, Do not use quote placeholders in your SQL strings. The above example can be re-written as â
sql_query = "SELECT * FROM users WHERE username = '%s';"
cursor.execute(sql_query, username)
Similarly, you should always use parameterized queries while using .extra()
and RawSQL()
.
Read more about it here
2. CRLF Injection
Web servers return a response containing both the HTTP headers and the response body. Headers and main content are separated by a combination of carriage return and line feed(CR-LF) characters. In a CRLF injection attack, the attacker inserts CRLF characters into the user input form or an HTTP request to trick the web server or web application into thinking that the header ends here and the main content begins.
There are 2 most common uses of CRLF injection attacks:
a) Log poisoning. In this attack, the attacker modifies the log file entries by inserting â\nâ and an extra line of text. This can be used to hide other attacks or to confuse system administrators by damaging the structure or formatting of the log file.
Take the example of a log file that has a pattern IPAddress â time â URL. E.g.106.217.24.193 - 07:26 - /author/book/?sort_by=rating
and an attacker managed to insert CLRF characters in the URL, the above log file would look like below106.217.24.193 - 07:26 - /author/book/?sort_by=rating&%0d%0a87.200.169.16 - 13:45 - /author/profile
b) HTTP response splitting (AKA header injection)
Web application frameworks and servers might also allow attackers to inject newline characters in headers to create a malformed HTTP response. In this case, the application would be vulnerable to attacks like HTTP Response Splitting/Smuggling.
Consider below examples-
from django.http import HttpResponse
def my_view(request):
content_type = request.META.get("CONTENT_TYPE")
response = HttpResponse()
response['Content-Type'] = content_type # creating/Modifying the response header
return response
Here content_type
hasnât been validated and an attacker can pass any malicious header that includes â\nâ in it. As a best practice, applications that use user-provided data to construct the response header should always validate the data first. So instead of the above implementation, weâd use the below approach.
from django.http import HttpResponse
def my_view(request):
content_type = request.META.get("CONTENT_TYPE")
response = HttpResponse()
if content_type in ALLOWED_CONTENT_TYPES:
response['Content-Type'] = content_type
else:
# default content type
response['Content-Type'] = "application/json"
return response
By exploiting a CRLF injection an attacker can also insert HTTP headers which could be used to defeat security mechanisms such as a browserâs XSS filter or the same-origin-policy.
Django handles header injection for emails sent out of the box If any subject
, from_email
or recipient_list
contains a newline, the email function (e.g. send_mail()
) will raise BadHeaderError
from django.core.mail import BadHeaderError, send_mail
try:
send_mail(subject, message, from_email, to_emails)
except BadHeaderError:
return HttpResponse('Invalid header found.')
Alternatively, you can explicitly check for these characters. This is what Django does under the hood.
if '\n' in input_text or '\r' in input_text:
raise BadHeaderError('Header cannot contain CLRF characters')
CRLF injection vulnerabilities are usually mitigated by Django automatically. Even if the vulnerability is not mitigated, it is easy to fix by following steps.
- Update the code so that content provided by the user is never used directly in the HTTP stream.
- Remove any CR-LF characters before passing content into the HTTP headers.
- Encode the data that you pass into HTTP headers. This will encode the CR & LF if the attacker attempts to inject them.
3. Timing Attack
As per Wikipedia, a timing attack is an attack in which the attacker attempts to compromise a cryptosystem by analyzing the time taken to execute cryptographic algorithms.
What does that mean? An attacker would supply different inputs to the system and observe the precise time taken by the system to respond to each input. Letâs understand this by a simple example.
Suppose you have a function that checks the API key or token provided by the user.
def is_valid_key(api_key):
return api_key == SECURELY_STORED_API_KEY
Here the problem is in the == operator. The way Python compares string under the hood is byte by byte(or character by character) and it gets terminated as soon as it finds a non-matching byte before iterating over the whole string. If the first character(or byte) of the input api_key string is different than the first character of the SECURELY_STORED_API_KEY string, it will return the result comparatively faster than the case where both strings are equal.
to understand this here is a naive example. The comparison between âabcdefghâ and âxbcdefghâ would return results faster than the comparison between âabcdefghâ and âabcdefghâ.
Attackers can use this to determine the bytes one by one and eventually a valid string.
How can you make sure that your function always takes constant time no matter what input the user provides? Again, Django to the rescue!
Django provides a nice utility function(constant_time_compare). Django internally uses this function to compare the password for authentication.
from django.utils.crypto import constant_time_compare
constant_time_compare(string1, string2)
If you want to, for some reason, implement it by yourself in Python, you can do â
def compare(string1, string2):
if len(string1) != len(string2):
return False
result = 0
for a, b in zip(string1, string2):
result |= ord(a) ^ ord(b) # XOR of Ascii value of characters
# We are doing an OR operation between the `result` and XOR result.
# If both strings are equal, XOR will be 0 for all characters and
# thus result would be 0
return not result # Equivalent to: return result == 0
This loop will keep running even if it finds the non-matching character and hence will always take the same time to compute and return the result.
Read also: https://docs.python.org/3/library/hmac.html#hmac.compare_digest
4. Clickjacking Attack
Clickjacking (click + hijacking), AKA UI redress attack, or UI redressing, is a type of attack where a malicious site wraps another site in an invisible frame. This attack can trick the user into clicking a webpage element that is invisible or disguised as another element for malicious purposes.
Modern browsers use the X-Frame-Options HTTP header that indicates whether or not a resource is allowed to load within a frame or iframe.
Django provides a middleware to guard against it.
MIDDLEWARE = (
...
'django.middleware.clickjacking.XFrameOptionsMiddleware',
...
)
In Django 3.0 or above, by default, it will set the X-Frame-Options header to DENY. On the other hand, in Django < 3.0, the default value for this header is SAMEORIGIN.
DENY means, your site cannot be used in any frame/iframe on any site.
SAMEORIGIN means, only your site is allowed to use itself in a frame.
Nevertheless, if you want to change the value, you can use X_FRAME_OPTIONS setting to set the desired value.
X_FRAME_OPTIONS = 'DENY' # in settings.py
Django also provides some decorators to do it per view basis. This is extremely useful when you want custom behavior per view. These decorators will override the middleware setting.
from django.views.decorators.clickjacking import (
xframe_options_deny,
xframe_options_exempt,
xframe_options_sameorigin,
)
@xframe_options_exempt
def view1(request):
return HttpResponse("This page is safe to load in a frame on any site.")
@xframe_options_deny
def view2(request):
return HttpResponse("Don't display in any frame, anywhere!")@xframe_options_sameorigin
def view3(request):
return HttpResponse("Display in a frame if it's from the same origin as me.")
As a best security practice always set this value to DENY.
5. Cross-Site Scripting (XSS)
In XSS attacks, an attacker injects scripts into the browsers of other users. This is usually achieved by storing the malicious scripts in the database and then serving them to the targeted users when they request the database. Weâll see different approaches Django provides to prevent these attacks.
a) Validating user input / HTML escaping
By default, Django templates protect you against the majority of XSS attacks by applying HTML escaping to the output of all template variables but thatâs not enough and you should always explicitly sanitize user-provided data. Consider the below example â
name = request.GET.get('name')
html = '<p>Hello, My name is %s</p>' % name
If the user provides below JS code in the name param, this will be executed on the page where we are rendering this HTML.
<script type="text/javascript">alert("Error")</script>
b) Enable browser detector
(In older versions of Django)
If your application still supports old browsers, you should consider having SECURE_BROWSER_XSS_FILTER setting turned on. This header tells the browser to enable the auto XSS attack detector feature. This is redundant in modern browsers because they donât consider the X-XSS-Protection HTTP header anymore.
c) Protect Cookies
Another small step you can take is to enable SESSION_COOKIE_HTTPONLY settings. By default, itâs True so no need to do anything here but if in the past, you have disabled it for some reason itâs a good time to revisit it. Having this setting enables cookies to be accessible on only HTTP(S) requests, Javascript canât access cookies.
But wouldnât it be great if there was a way that allows us to execute only our siteâs(verified) Javascript?
d) Content-Security-Policy
You can do that by setting the Content-Security-Policy (CSP) HTTP header. Browsers will not load any JS, CSS, or Images that are not permitted by the CSP header, including inline JS. How can we do that using Django?
Django doesnât have any inbuilt support for this but you can use a popular 3rd party package called django-csp, maintained by Mozilla.
Add CSP middleware provided by this library
MIDDLEWARE = (
...
'csp.middleware.CSPMiddleware',
...
)
And add the below configuration to your siteâs settings. Note that this can vary based on your siteâs requirements.
CSP_DEFAULT_SRC = ("'self'", 'cdn.example.net')
CSP_STYLE_SRC = ("'self'", 'fonts.googleapis.com')
CSP_SCRIPT_SRC = ("'self'",)
CSP_IMG_SRC = ("'self'",)
CSP_FONT_SRC = ("'self'",)
# For more CSP settings, please visit
# https://django-csp.readthedocs.io/en/latest/configuration.html#policy-settings
In the end, Iâd say preventing an XSS attack is tricky and no single approach will fully protect you instead use a combination of multiple approaches mentioned above.
6. Cross-Site Request Forgery (CSRF)
AKA one-click attack or session riding or XSRF, sometimes pronounced âsea-surfâ, is an attack where a user is tricked by an attacker into submitting a web request that they did not intend.
Consider a simple example where you can send money from your bank account to another by simply filling a form that takes two fields, receiverâs account number and the amount you want to send. In this case, an attacker can send you a hidden form through an email or a web link and If you click on that link while you are logged in to your bank account, you un-intentionally send money to the attackerâs account. Similarly, The attacker can trick you into sending your active cookies and later attacker can use them to gain access to the vulnerable site.
Using a CSRF token is a robust safeguard against this attack. A CSRF token is a unique token generated by the application for each session or request. Django provides a very easy way to Include CSRF tokens in your forms. There is middleware for this in Django that by default will be added to your MIDDLEWARE setting. âdjango.middleware.csrf.CsrfViewMiddlewareâ. What else you need to do is, if you are using Djangoâs templating system, use the csrf_token (for Jinja2 itâs csrf_input) tag inside the form.
<form method="post">{% csrf_token %}...</form>
Additionally, you can turn on the below settings to protect CSRF cookies.
CSRF_COOKIE_SECURE = True # cookie will only be sent over an HTTPS connection
CSRF_COOKIE_HTTPONLY = True # only accessible through http(s) request, JS not allowed to access csrf cookies
To read more about CSRF protection in Ajax, exempting specific views from CSRF protection and other limitations, please visit official csrf docs.
7. HTTP Strict Transport Security (HSTS)
Always serve your site over a secure connection, Always use SSL!!!
Django provides a security middleware that can help you set a few things up quickly.
MIDDLEWARE = (
...
'django.middleware.security.SecurityMiddleware',
...
)
We are going to discuss particularly 3 important settings this middleware offers.
a) SECURE_SSL_REDIRECT
If True, the SecurityMiddleware redirects all non-HTTPS requests to HTTPS. The default is False.
SECURE_SSL_REDIRECT = True
But the problem is that if the user had already initiated the insecure request(HTTP) we redirected to HTTPS. and an attacker manages to intercept that insecure request, things still can go wrong. How can we prevent that request from happening altogether?
HSTS is the way to go!
b) SECURE_HSTS_SECONDS
Whenever a user makes an HTTP request to your site, HSTS sends a header in the response that tells the browsers to always use an HTTPS connection to your site. Although as a prerequisite for this, It requires all your statics, scripts, media, fonts, and everything to be served over HTTPS, otherwise users wonât be able to connect to the site. The default value of this setting is Zero.
SECURE_HSTS_SECONDS = 31536000 # seconds in a year: 365*24*60*60
Ideally, you should set a large value such as 31536000 seconds which is equal to 1 year. But if you already donât have itâs recommended to start with a low value e.g. 86400 because if something is not right with your sites, browsers wonât be able to connect to your site for this long. You can always update this number once you are sure everything is fine. Before making any changes go through the documentation.
c) SECURE_HSTS_INCLUDE_SUBDOMAINS
Setting it to True will ensure that all subdomains, not just top-level domains, can only be accessed over a secure connection. The default is False.
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
8. Session Hijacking
To mitigate this vulnerability we need to enable two settings.
a) SESSION_COOKIE_SECURE = True
From the doc, If this is set to True
, the cookie will be marked as âsecureâ, which means browsers may ensure that the cookie is only sent over an HTTPS connection. By setting this, we are ensuring that an attacker wonât capture an unencrypted session cookie and use the cookie to hijack the userâs session.
b) SESSION_COOKIE_HTTPONLY = True
If this is set to True, client-side JavaScript will not be able to access the session cookie.
E.g. if this is set to False,
<SCRIPT>
alert(document.cookie);</SCRIPT>
9. Denial of Service (DoS)
- Securing your system against attacks like brute force and DoS is vital. Implementing throttling and rate limiting is a powerful defense. You can set these limits at the server level and within specific views or APIs. Tools like Django Rest Framework (DRF) and Django Ratelimit offer efficient ways to apply these limits, safeguarding your system from excessive requests and potential threats.
- Few other advantages of having throttling and rate-limit -
1) Limiting the rate of incoming requests can prevent a network or server from being overwhelmed by excessive traffic. This ensures that performance and availability are not adversely affected by a sudden surge in requests.
2) This subsequently improves user experience by reducing delays and improving the responsiveness of a network or server.
3) Further, it can help to avoid extra costs by preventing the overuse of resources
Here is an example of how youâd do in DRF -
REST_FRAMEWORK = {
'DEFAULT_THROTTLE_CLASSES': [
'rest_framework.throttling.UserRateThrottle'
],
'DEFAULT_THROTTLE_RATES': {
'user': '1000/day' # Allow only 1000 requests per day for authenticated users
}
}
10. Other Django best practices to improve app security đ
- Always use DEBUG = False on production
- Find out Security Misconfiguration by running
python manage.py check --deploy
- Sensitive data shouldnât be logged without encryption. use
@sensitive_variables
and@sensitive_post_parameters
decorator to mask out sensitive information. For more details, refer to: https://docs.djangoproject.com/en/dev/howto/error-reporting/#filtering-sensitive-information - Django should never serve static files, instead, the webserver(nginx/apache) should.
- Have sufficient Logging and monitoring to detect security incidents immediately.
- lastly, use the latest libraries and update on security releases whenever available.
Conclusion
What we learn from these vulnerabilities is, to always validate and sanitize the user-provided data, such as URL parameters, POST data payloads, headers, and cookies. A thumb rule of security is that user-provided data should always be considered untrusted and sanitized/escaped accordingly.
I hope you enjoyed the post. If you think this can be useful to others, Donât hesitate to share it with them. Drop a comment if you want to see Django in action for a specific web vulnerability.
For more such articles, follow me at: https://gauravvjn.medium.com.