Password reset poisoning - Python

Password reset poisoning - Python

Need

Secure password reset functionality

Context

  • Usage of Python 3 for writing and executing Python code
  • Usage of Django for building web applications in Python

Description

Non compliant code

        from django.contrib.auth.tokens import default_token_generator
from django.core.mail import send_mail
from django.contrib.sites.shortcuts import get_current_site
from django.utils.http import urlsafe_base64_encode
from django.template.loader import render_to_string
from django.utils.encoding import force_bytes
from django.contrib.auth.models import User

def password_reset(request):
    email = request.POST['email']
    user = User.objects.get(email=email)
    token = default_token_generator.make_token(user)
    domain = get_current_site(request).domain
    uid = urlsafe_base64_encode(force_bytes(user.pk))
    password_reset_url = f'http://{domain}/reset/{uid}/{token}'
    message = render_to_string('reset_password_email.html', {
        'password_reset_url': password_reset_url
    })
    send_mail('Password reset on %s' % domain,
              message, None, [user.email])
        
        

The above code is a Django view function that is vulnerable to password reset poisoning.

When a user requests a password reset, the application generates a unique URL that the user can use to reset their password. This URL is sent to the user's email address. The URL contains a token that is generated based on the user's details and is used to verify the identity of the user when they click on the link.

The vulnerability lies in the use of the get_current_site(request).domain method to get the domain for the password reset URL. This method gets the current site from the request object, which can be manipulated by an attacker.

An attacker can change the Host header in the HTTP request to a domain that they control. When the application generates the password reset URL, it will use the attacker's domain instead of the legitimate one. The attacker can then intercept the password reset URL and use it to reset the user's password.

This is a serious vulnerability as it can lead to account takeover attacks. The attacker can gain unauthorized access to the user's account and perform actions on their behalf.

Steps

  • Validate the domain name to ensure it's from a trusted source before generating the password reset link.
  • Use HTTPS instead of HTTP for the password reset link to ensure the communication is secure.
  • Consider using a framework's built-in functions for password reset functionality, as they often have built-in protections against such vulnerabilities.
  • Ensure that the token generated for password reset is tied to the user's session and cannot be used if the session ID changes.
  • Implement rate limiting on password reset requests to prevent brute force attacks.

Compliant code

        from django.contrib.auth.tokens import default_token_generator
from django.core.mail import send_mail
from django.contrib.sites.shortcuts import get_current_site
from django.utils.http import urlsafe_base64_encode
from django.template.loader import render_to_string
from django.utils.encoding import force_bytes
from django.contrib.auth.models import User
from django.conf import settings
from django.core.exceptions import ValidationError
from django.http import HttpResponseBadRequest

def validate_domain(domain):
    if domain not in settings.ALLOWED_HOSTS:
        raise ValidationError("Untrusted domain")

def password_reset(request):
    email = request.POST['email']
    user = User.objects.get(email=email)
    token = default_token_generator.make_token(user)
    domain = get_current_site(request).domain
    validate_domain(domain)
    uid = urlsafe_base64_encode(force_bytes(user.pk))
    password_reset_url = f'https://{domain}/reset/{uid}/{token}'
    message = render_to_string('reset_password_email.html', {
        'password_reset_url': password_reset_url
    })
    send_mail('Password reset on %s' % domain,
              message, None, [user.email])
        
        

The updated code includes a function validate_domain that checks if the domain is in the list of ALLOWED_HOSTS in the settings. If the domain is not in the list, a ValidationError is raised. This ensures that the password reset link is only generated for trusted domains.

The password reset link now uses HTTPS instead of HTTP. This ensures that the communication between the client and the server is secure and the password reset link cannot be intercepted.

The default_token_generator function from Django's auth tokens is used to generate a unique token for the password reset. This token is tied to the user's session and cannot be used if the session ID changes.

The send_mail function is used to send the password reset email to the user. The email contains the password reset link with the secure token.

The password reset request is tied to the user's session, and rate limiting should be implemented at a higher level (e.g., at the web server or load balancer level) to prevent brute force attacks.

References