Skip to main content

User enumeration

Need

Prevention of user enumeration

Context

  • Usage of Python 3 for developing Python applications
  • Usage of Django for building web applications in Python

Description

Non compliant code

from django.contrib.auth import authenticate

def login(request):
username = request.POST['username']
password = request.POST['password']
user = authenticate(request, username=username, password=password)
if user is not None:
login(request, user)
return HttpResponse("Login successful")
else:
try:
User.objects.get(username=username)
return HttpResponse("Wrong password")
except User.DoesNotExist:
return HttpResponse("User does not exist")

This Django view function handles user authentication. It first attempts to authenticate a user with the provided username and password. If the user object is not None, meaning the authentication was successful, it logs in the user and returns a successful message.

However, if the authentication fails, it tries to get a user with the provided username. If it successfully retrieves a user, it returns an error message indicating that the password was incorrect. If it fails to retrieve a user, it returns an error message indicating that the user does not exist.

This is a security vulnerability because it allows an attacker to enumerate valid users. If an attacker attempts to log in with a random username and receives the "Wrong password" error, they know that the username exists in the system. If they receive the "User does not exist" error, they know that the username does not exist. This allows them to build a list of valid usernames, which they can then use in further attacks.

The difference in response time between the two error messages could also potentially be used to enumerate users, as querying the database for a user that doesn't exist may take longer than retrieving a user that does. This difference in response time could be used by an attacker to determine whether a username exists or not.

Steps

  • Remove the specific error messages that indicate whether the username exists or not. Instead, use a generic error message such as 'Invalid username or password'. This way, an attacker cannot determine if a username exists based on the error message.
  • Implement rate limiting on login attempts to prevent brute force attacks. This will limit the number of attempts a user can make within a certain time period.
  • Consider using a delay in the response. This can help to prevent user enumeration as it makes the process time-consuming for an attacker.
  • Implement a lockout policy. After a certain number of failed attempts, the account should be locked for a certain period of time.
  • Use multi-factor authentication. This adds an additional layer of security as even if an attacker guesses the password, they would still need the second factor to gain access.

Compliant code

from django.contrib.auth import authenticate
from django.core.exceptions import ValidationError
from django.contrib.auth.decorators import ratelimit
from time import sleep

@ratelimit(key='ip', rate='10/m') # Limit to 10 attempts per minute per IP
def login(request):
username = request.POST['username']
password = request.POST['password']
user = authenticate(request, username=username, password=password)
if user is not None:
login(request, user)
return HttpResponse("Login successful")
else:
sleep(1) # Delay response
return HttpResponse("Invalid username or password") # Generic error message

The updated code includes several measures to prevent user enumeration and brute force attacks:

  1. Generic Error Message: The specific error messages indicating whether the username exists or not have been replaced with a generic error message 'Invalid username or password'. This prevents an attacker from determining if a username exists based on the error message.

  2. Rate Limiting: The ratelimit decorator from Django's django.contrib.auth.decorators is used to limit the number of login attempts to 10 per minute per IP address. This helps to prevent brute force attacks by limiting the number of attempts a user can make within a certain time period.

  3. Response Delay: A delay of 1 second is introduced in the response using Python's time.sleep function. This can help to prevent user enumeration as it makes the process time-consuming for an attacker.

Note: This code does not include a lockout policy or multi-factor authentication. Implementing these would require additional changes to your authentication system and are not included in this simple example. However, they are recommended for a robust authentication system.

References