Fantastic Vulnerabilities and Where To Find Them (Part 1) — Cross-site Scripting with Django form errors

XSS scripting vulnerabilities

Victor Chibotaru
Mar 14, 2019 · 4 min read
Image for post
Image for post

Why is Cross-Site Scripting (XSS) still the most common web vulnerability? The theory of identifying XSS is pretty straightforward, there are many static analysis tools created to detect it and yet there are so many undiscovered vulnerabilities. So, what gives?

Well, one of the reasons is that traditional program analysis methods often fail to identify the intent of a given piece of code. For example, a tool might struggle with figuring out which objects in the program could contain user input.

In my previous post I described how we addressed this problem by building a system that learns security specifications from thousands of Open Source projects and uses them to find real vulnerabilities. I also promised to share some cool examples of what it learned.

I decided to start with an interesting and rather unexpected source of possible issues in projects using Django. This post is a guide to identifying and exploiting XSS vulnerabilities using validation errors in Django forms. Here’s a real example:

Image for post
Image for post

Let’s jump straight into it and start with a little quiz. How many times have you written/seen code similar to the following snippet?

What about this one?

Or this one?

All of them are widespread examples of how you usually let the user know that the provided input is invalid, right? The input is taken from the HTTP request parameters and neatly unmarshalled into a MyForm object. If any of the fields contains invalid input (e.g. someone entered the string "foobar" into a numeric field), then a 400 Bad Request page gets returned with a description of the error. The difference between snippets is the format of the returned error — an HTML list, plain text or JSON.

Now a million dollar question — which of these snippets will make your web app XSSable?

To answer it, let’s investigate the Django forms API from two points of view:

  1. Is the attacker able to inject malicious input into the web page displayed in user’s browser?
  2. Will this malicious input always be properly escaped before it gets returned to the user?
Image for post
Image for post

According to Django’s documentation the way to build dynamic error messages for field validation errors is to raise a django.core.exceptions.ValidationError exception with the corresponding message. Such an exception thrown from any of the validation functions of the form (e.g. the methods clean() and clean_<fieldname>()of the django.forms.BaseForm class) will cause the message to be stored in the form’s error dictionary (django.forms.utils.ErrorDict) and later possibly shown to the user.

One way to exploit such an exception is to use some of the built-in form fields that conveniently reflect the faulty input into the exception message. I tried all Django form fields types listed here and got the following list: ChoiceField, TypedChoiceField, MultipleChoiceField, FilePathField. Each of these generates an error message like "Select a valid choice. %(value)s is not one of the available choices.” , where value is the faulty input. <script>alert(1)</script> for the win.

The second option is to exploit custom fields and/or validation procedures. For example, consider the following snippet (taken from a real project and modified for brevity):

Here a good payload would be something like foo.<img src=x onerror=alert(1)>.

Yep, you’re right, the ValidationError exceptions alone will not grant us an XSS. For a proper vulnerability we need one more ingredient — the ability to inject error messages into the final HTML page that gets returned to the user.

The aforementioned ErrorDict class has the following methods to extract the error messages:

  1. as_data() — no sanitization
  2. get_json_data(escape_html=False) — no sanitization if escape_html == False (the default)
  3. as_json(escape_html=False) no sanitization if escape_html == False (the default)
  4. as_ul() — safe
  5. as_text() — no sanitization
  6. __str__() (calls as_ul()) — safe

Now let’s go back to our little quiz. It’s easy to see that snippet 1 is safe, because it uses the __str__() method, which escapes the input. Snippets 2 and 3, however are dangerous and may result in XSS.

There are two main takeaway messages here. The one for developers is the mantra “always sanitize untrusted input”. The one for security researchers is: a simple grep -R "ValidationError" might broaden the attack surface for you.

By the way you have my respect if you passed the quiz correctly without reading the full post.

Image for post
Image for post
Image for post
Image for post

Building A Web Or Mobile App?

Crowdbotics is the fastest way to build, launch and scale an application.

Developer? Try out the Crowdbotics App Builder to quickly scaffold and deploy apps with a variety of popular frameworks.

Busy or non-technical? Join hundreds of happy teams building software with Crowdbotics PMs and expert developers. Scope timeline and cost with Crowdbotics Managed App Development for free.


The fastest way to build your next app.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store