XSS in django-allauth <0.63.6

This post details a Cross-Site Scripting (XSS) vulnerability I discovered in django-allauth, a popular Django package for authentication. This vulnerability affected the Facebook provider only, and it was fixed in version 0.63.6 on July 12, 2024.

Background

Before I found this vulnerability, I already reported another one to django-allauth, a login CSRF vulnerability in its SAML provider, which was fixed in version 0.63.3 (maybe I'll write a post about it if people are interested in more posts like this).

At Read the Docs, we use django-allauth for user authentication. I was in charge of integrating SAML into our authentication system, while working on that I noticed the CSRF vulnerability. After reporting it and seeing how quick it was fixed, I decided to do a quick security audit of the project.

The vulnerability

After grepping the codebase for common vulnerable patterns, I found this line of code that caught my attention:

django-allauth allows using Facebook as a provider for social authentication, and allows using the regular form (oauth2 method) or the Facebook JavaScript SDK (js_sdk method) to login. When using the js_sdk method, the fb_data variable is passed to the template to be used in the frontend.

Which is then used in the fbconnect.js script to initialize the Facebook SDK.

So, what's the problem here? The fb_data variable is marked as safe (Django won't escape it when including it in a template) after being transformed into a JSON string. Since json.dumps doesn't escape HTML characters, it's possible to inject arbitrary HTML and JavaScript code into the template.

Exploitation

Using mark_safe by itself is not a vulnerability, as long as the content is trusted and doesn't contain user input. So the next step was to find a way to inject user-controlled content into the fb_data variable.

As shown below, most of the content in the fb_data variable is static:

Except for loginOptions, which includes the value from the scope query parameter:

With that, we now can inject arbitrary HTML and JavaScript using the scope query parameter.

But wait... In which page can we control the scope query parameter? By following the code, I found that media_js is used in the providers_media_js template tag, which is called in the login_extra.html snippet, and furthermore that snippet is included anywhere the social providers are listed, like the login page (/accounts/login/), and the social account connections page (/accounts/3rdparty/).

Payload

To exploit this vulnerability, an attacker could inject the following content in the scope query parameter:

</script><script>alert(document.domain)</script><script>

What this does is:

  • Closes the script tag containing the fb_data variable.

  • Injects a script that shows an alert with the current domain.

  • Opens a new script tag, so the rest of the JSON content is not shown as plain text.

Proof of concept

I created a proof of concept to demonstrate the vulnerability, so you can see it in action, you just need to have Python and uv installed:

It consists of a simple Django project with django-allauth==0.63.5 installed, and a Facebook provider configured using the JavaScript SDK.

$ git clone https://github.com/stsewd/poc-xss-django-allauth
$ cd poc-xss-django-allauth
$ uv run manage.py migrate
# Create a user to log into the application.
$ uv run manage.py createsuperuser
$ uv run manage.py runserver
XSS in login page:
  • While logged out, go to http://127.0.0.1:8000/accounts/login/?scope=</script><script>alert(document.domain)</script><script>.

XSS in social connections page:
  • Go to http://127.0.0.1:8000/accounts/login/.

  • Log in with the user you created.

  • Go to http://127.0.0.1:8000/accounts/3rdparty/?scope=</script><script>alert(document.domain)</script><script>.

Showing an alert is just a simple example, but an attacker can execute any JavaScript code in the context of the user's session.

Mitigation

You should never mark user-controlled content as safe, but if you find yourself wanting to include JSON content in a template, escaping will break the JSON format.

Luckily, Django has a built-in template filter to include JSON content in a template safely, json_script. Sadly, that filter wasn't available at the moment the allauth code was written, but it's been available since Django 2.1, since allauth supports newer versions of Django, it was possible to use it, as you can see in the fix.

Timeline

  • 11/07/2024: Found and reported the vulnerability to django-allauth.

  • 12/07/2024: Maintainer confirmed the vulnerability and released version 0.63.6 with the fix.

Acknowledgements

  • I'm always surprised by how quickly open source maintainers fix security vulnerabilities (so much faster than commercial software vendors), kudos to Raymond Penners, maintainer of django-allauth.

  • It's also great I have the support at Read the Docs to spend part of my work time on security audits on packages we use. Even if the vulnerabilities don't affect our systems directly (we don't use the Facebook provider), it's nice to have the chance to give back to the community.

Are you still using django-allauth <0.63.6? The fix was released more than 6 months ago, please update your dependencies! Thank you for reading, and let me know if you'd like to see more posts like this!

Comments

Comments powered by Disqus