XSS Security in Django

You know output encoding is most important thing when topic comes to XSS. Today I’ve decided to write a article about how to secure your Django application against XSS vulnerabilities. Handing XSS cases with Django is more easy than other frameworks. Your Django app is approximately secure against XSS even if you developed it without security  mind. Therefore Django is try to encode specific characters in order to prevent yourself from cross-site scripting. But Django going to be fail  under the some circumstances.

PS : If you think any part of this article needs update or changes, feel free to leave comment. 

Django is a high-level Python Web framework that encourages rapid development and clean, pragmatic design.

Django Default Output Encoding Mechanism

Django try to encode some dangerous characters  in order to prevent XSS vulnerability. Let see which characters are going to be encoded when you try to print it out at htmls.

from django.shortcuts import render, render_to_response

# Create your views here.
def home(request):
    foo = request.GET["param"]
    return render_to_response('home.html', locals())

And home.html file.

<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
    <title></title>
</head>
<body>
My name is : {{ foo }}
</body>
</html>

We all know that most common XSS cases related to quotes, less-bigger than sign and brackets. Therefore we need to know which characters are going to be encoded. In order to understand that I create following URL

http://127.0.0.1:8000/?param=’-“-`->-<-(-)-{-}-%26

Result is here.

<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
    <title></title>
</head>
<body>
My name is : &#39;-&quot;-`->-<-(-)-{-}-&amp;
</body>
</html>

I’ve append dash after every single characters so we can easily understand which output belongs to which character.

As a result, we’ve learned that Django only cares following characters.

  • Single and double quote.
  • Less-bigger than signs.

Test Django Output against Different Context Based XSS Cases

We all know XSS vulnerabilities can be appear in different contexts which means that each context have individual payload / approach. I’ve create following html file in order to show differences to you.

<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
    <title></title>
</head>
    <body>

    <!-- HTML Context -->
    My name is : {{ foo }}
    
    <!-- Attribute Context -->
    <input name="x" value="{{ foo }}"/>

    <!-- Javascript Context -->
    <script> var foo = "{{ foo }}"</script>   

    <!-- Style Context -->
    <div style="{{ foo }}">Style Context</div>

    </body>
</html>

Let’s send same request again and analyze output context by context.

<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
    <title></title>
</head>
    <body>

    <!-- HTML Context -->
    My name is : &#39;-&quot;-`->-<-(-)-{-}-&amp;

    <!-- Attribute Context -->
    <input name="x" value="&#39;-&quot;-`->-<-(-)-{-}-&amp;"/>

    <!-- Javascript Context -->
    <script> var foo = "&#39;-&quot;-`->-<-(-)-{-}-&amp;"</script>

    <!-- Style Context -->
    <div style="&#39;-&quot;-`->-<-(-)-{-}-&amp;">Style Context</div>

    </body>
</html>

Html Context

As you can see < and > sings are encoded by Django. Therefore we are not able to initiate new html tag like <img or <script> . You Django application is secure against HTML Context Based XSS Vulnerabilities.

Attribute Context

Single and double quotes are encoded which  is good for us. Attribute context based XSS vulnerabilities appears because lack of single/double output encoding. But this is not enough. We already know that Django doesnt encode  back-tick  ( `) which is cause XSS vulnerability in specific IE version.

Let send following request to Django application and analyze output.

// Request : http://127.0.0.1:8000/?param=``onmouseover=prompt(1)

// Output

<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
    <title></title>
</head>
    <body>

    <!-- Attribute Context -->
    <input name="x" value="``onmouseover=prompt(1)"/>

    </body>
</html>

I’ve omitted other context and leave attribute context sample alone. As you can see  “  are not encoded over there. This doesn’t means anything to Firefox and Chromes but it does for old version of IE. Because old IE browsers treat it as a valid separator.

 Javascript Context

Single, double and less-bigger than sings are encoded. Which means we cant reach out of variable values. Because we need single or double quotes but they are already encoded by Django. Also we are not able to end script tags because of > and < are encoded too.

Payload = "; alert(1);f="

OR

Payload = '; alert(1);f='

OR

</script><script>alert(1)</script>

All of these payload will be failed because of Django encode but there is some specials cases about Javascript context.

Keeping old things is bad …

Let assume that developers comment out some part of JS codes. I’ve seen this too many times during penetration test.

<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
    <title></title>
</head>
    <body>

    <script>
        //var f = callSomeFunction('{{ foo }}')
    </script>
</html>

It just comment outed. This codes are vulnerability because of comment out.

// URL : http://127.0.0.1/?param=TEST%0Aalert%281%29%3B//

// Result
<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
    <title></title>
</head>
    <body>

    <script>
        //var f = callSomeFunction('TEST
alert(1);//')
    </script>
</html>

Boom.  The trick is new line character. %0A is encoded new line character. When python see that it create new and continue to printing values . As an attacker we avoid comment out area and reach to javascript context. Last double slash is for prevention syntax error.

 Style Context

Obviously Django can be vulnerable against stylish XSS. Because Django doesn’t encodes ( and ) . Let’ me show you some payload that can successfully exploit XSS vulnerability. Please send following request to the django app.

// Request : http://127.0.0.1:8000/?param=width:expression(alert(1))

// Result : 

<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
    <title></title>
</head>
    <body> 
 
    <!-- Style Context -->
    <div style="color:expression(alert(1))">Style Context</div>
 
    </body>
</html>

I’ve omitted other contexts again. Now you see payload reflected inside style attribute of div tag and this is valid XSS vulnerability works on Internet Explorer.

Mitigation

If you use default output methods in everywhere, your Django app will be vulnerable against XSS. From previous section we know back-tick can be dangerous on attributes. We should encode it out at attribute context. Also we’ve learned that new line characters is dangerous under the circumstances, we should encode it too. And Bracket is can be dangerous too when input reflected inside style context.

I’ve developed 3 differend function. Please read instructions.

@register.filter('attr_encode', is_safe=True)
@stringfilter
def attribute_context_encode(value):
    """
    How to use : <input name="fname" value="{{ data|attr_encode }}">
    Django encodes double and singlue quotes as default. But this is not enought for XSS prevention.
    e.g: ``onmouseover=prompt(1) is valid XSS vector for old IE versions.
    """
    return value.replace('`', '&grave;')


@register.filter('js_encode', is_safe=True)
@stringfilter
def js_context_encode(value):
    """
    How to use : <script> var name = "{{ data|js_encode }}"; </script>
    Django encodes double and singlue quotes as default which means input can not break context.
    e.g :  "; confirm(1); "
    e.g :  '; prompt(1); //
    e.g :  </script><script>alert(1)</script>
    New line character may break the page which is cause XSS under the circumstances.
    e.g :   foo%0Aalert(1);//   --> %0A is \n
    """
    return value.replace('\n', '')

@register.filter('css_encode', is_safe=True)
@stringfilter
def style_context_encode(value):
    """
    How to use : <span style="{{ data|css_encode }}"></span>
    Bracket sign is encoded because following XSS Payload is valid for old version of IE
    e.g : width:expression(alert(1))
    """
    return value.replace('(', '&lpar;').replace("\\", "&bsol;")

Furter and detailed information can be found at Ashar’s work and PHP implementation : https://github.com/symphonycms/xssfilter/blob/master/lib/xss.php