django-unsubscribe
django-unsubscribe copied to clipboard
SECURITY RISK: Unsubscribe URL token can expose a site's SECRET_KEY
TL;DR
All developers using this package should immediately stop using it, and developers should not use this package until this issue is fixed and previous insecure versions removed from PyPI (to prevent downgrade attacks).
Explanation
The get_token_for_user
function utils.py
:
def get_token_for_user(user):
# TODO: use USERNAME_FIELD instead
user_id = user.email.encode('utf-8')
secret = settings.SECRET_KEY.encode('utf-8') # <-- this line
return hashlib.md5(user_id + secret).hexdigest() # <-- and this line
Line 10 of utils.py pulls Django's SECRET_KEY
and hashes it with the user's ID with md5 to act as a nonce.
The Django documentation for SECRET_KEY
says:
Warning
Keep this value secret.
Running Django with a known SECRET_KEY defeats many of Django’s security protections, and can lead to privilege escalation and remote code execution vulnerabilities.
Since the md5 hash is highly parallelizable, once a user knows their ID 1, they can reverse the unsubscribe token if they have enough hardware and/or enough time. Once they have the unsubscribe token, they can extract the SECRET_KEY
from the reversed token, because the reversed md5 input is only the user's ID concatenated with the SECRET_KEY
.
Furthermore, switching the token to be the user's username will not fix this, because that information is choosable by the user, which obviates the need to figure out their user ID.
The Fix
The only proper way to create a nonce for the unsubscribe URL is to generate one from a cryptographically secure random number generator and store the generated nonce in the database with a foreign key to the user.
If you add me as a maintainer of this package on GitHub and PyPI I'm happy to fix it, release a secure version on PyPI, and delete the older versions from PyPI.
Notes
1 There are plenty of other URLs used by other Django packages that expose the user's ID to the user, and to other users, so the appropriate assumption is that the user can very easily ascertain their user ID.
Would using a different hashing algorithm that was secure by current computing standards not solve the issue too?
PBKDF2 and other secure password hashing algorithms hash a configurable number of times to continuously stay ahead of the curve. Hashing once would still allow a well funded attacker to reverse the hash. It would be more difficult but still perfectly possible.
The bottom line is: don't use secret keys for anything other than their intended purpose. It's intended for internal Django use; don't use it for anything else. Solve this problem properly so nobody has to worry about it.
Sorry to revive an old issue, but I thought I'd add my 2c. Though it's bad practice, the secret would key be pretty much impossible to crack with modern computing simply due to its length. According to http://calc.opensecurityresearch.com/, it would take 1.8149008607511732e+90 years to enumerate that search space at 8 billion hashes per second(a strong, single-gpu machine).
Using a secure hash function, a variant of SHA3 for example, will be more secure than storing this sensitive information in the database, in most cases.
PBKDF2 is NOT a secure hashing function (as you've noted). It is used for password hashing because it is fast and configurable, allowing systems to maintain performance while being able to easily scale for security based on the level of current technology.