Today’s Django Security Release Deconstructed (4.0.1, 3.2.11, and 2.2.26)

Gong! It’s like a Django security sprint!

Happy new year, and happy new upgrade! Django has issued a new security release today. This is the first set of security fixes that I’ve been involved in, so I thought I’d take the opportunity to explain the issues in a bit more depth.

I’d also like to surface and praise those working tirelessly behind the scenes to find and fix these problems! They truly help keep Django on top of its security game.

1. CVE-2021-45115: Denial-of-service possibility in UserAttributeSimilarityValidator

The UserAttributeSimilarityValidator password validator ensures that a provided password isn’t too similar to the user’s other attributes, such as their email address. It’s not a good idea to allow user@example.com to have password user@example.com, even with a few characters changed! The validator is active by default in the AUTH_PASSWORD_VALIDATORS setting from Django’s startproject template.

If a user posted a large password (100k+ chars) to a registration form, it could lead to several seconds of runtime in UserAttributeSimilarityValidator. This makes it a DoS vector, where an attacker making many registration requests could make your site unresponsive.

The fix avoids the comparison when the password is significantly longer than an attribute, as the similarity is guaranteed to be low.

Thanks to Chris Bailey for reporting. After team discussion, I made the initial PR with a different approach, Florian Apolloner suggested the current one, and Carlton Gibson and Mariusz Felisiak helped review.

2. CVE-2021-45116: Potential information disclosure in dictsort template filter

The dictsort template filter sorts a list of dicts or lists based upon an index or key. It also has a twin, dictsortreversed, that does the same but backwards, but I’ll only describe dictsort here.

dictsort can use keys in nested dicts or lists by using the .-separated paths. For example, you might use it to sort a list of dicts representing books like so:

{% for book in books|dictsort:"author.age" %}
    * {{ book.title }} ({{ book.author.name }})
{% endfor %}

In some use cases, you might use a “sort path” from a variable:

{% for book in books|dictsort:sort_path %}

If that variable was user-controllable, such as from a query parameter in request.GET, there was a chance for disclosure of private data. This is because dictsort didn’t only support dict keys and list indexes - it used Django’s internal Variable class, so it could access arbitrary attributes, and would even call functions.

The proof-of-concept used dictsort to sort a list of users based up on a user-controlled sort order. It successively sorted by each character of the password attribute, with sort paths password.0, password.1, and so on. With enough users the attack can then surmise the probable values of each user’s (hashed) password. Hashing provides a lot of protection, but the attack also applies to non-hashed private attributes, such as API keys.

The fix restricts the power of dictsort to only what the docs advertise: key lookups and indexing. It does this with a new resolution function with limited capabilities. I think this is a good example of “Don’t Repeat Yourself” (DRY) causing problems. The initial use of Variable made the implementation of dictsort shorter, but it gave it more power than intended.

Thanks to Dennis Brinkrolf for the report. Florian Apolloner authored the fix, and Mariusz Felisiak, Carlton Gibson, Markus Holtermann, Nick Pope, and myself reviewed.

3. CVE-2021-45452: Potential directory-traversal via Storage.save()

Django’s Storage class is the core of its file storage API. When storing a new file, the normal way is to use Storage.generate_filename(filename) to generate a collision-free name based up on the file’s original name.

generate_filename() blocks directory traversal: using a filename that includes .. in order to save the file in a different place inside the storage folder. For example, an attacker might try trigger a file save to ../users/12/profile.jpg to overwrite a different user’s profile picture. Directory traversal is mostly a concern with the built-in FileSystemStorage, rather than cloud-based storage options like the popular S3Boto3Storage class in django-storages.

Storage.save() accepts a filename to save a given file at. Unfortunately, it did not previously apply the filename validation to prevent directory traversal. (Traversal to outside the storage folder was blocked though, for all methods.)

The problem could occur if a file was written with a file like ..\users\12\profile.jpg. On Unix, which uses forward slash for directory separators, this is a valid filename in the same directory. But when saving such a filename in a Django FileField, the path would be normalized to ../users/12/profile.jpg, allowing path traversal when reading.

The fix adds that same validation to save().

Thanks again to Dennis Brinkrolf for the report. Florian Apolloner also wrote this fix, and Mariusz Felisiak and Carlton Gibson reviewed.

(Thanks also to Florian Apolloner for reviewing this section.)

Fin

May your upgrade be quick to deploy,

—Adam


Read my book Boost Your Git DX to Git better.


Subscribe via RSS, Twitter, Mastodon, or email:

One summary email a week, no spam, I pinky promise.

Related posts:

Tags: