django-unsubscribe icon indicating copy to clipboard operation
django-unsubscribe copied to clipboard

SECURITY RISK: Unsubscribe URL token can expose a site's SECRET_KEY

Open blag opened this issue 8 years ago • 4 comments

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.

blag avatar Oct 17 '16 01:10 blag

Would using a different hashing algorithm that was secure by current computing standards not solve the issue too?

KentShikama avatar Oct 17 '16 10:10 KentShikama

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.

blag avatar Oct 17 '16 11:10 blag

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).

Plazmaz avatar Jul 20 '17 15:07 Plazmaz

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.

DylanYoung avatar Dec 12 '17 21:12 DylanYoung