How to Remediate CWE-204: Observable Response Discrepancy

1/14/2024
Pasha Probiv
How to Remediate CWE-204: Observable Response Discrepancy

Observable Response Discrepancy is one of the most popular issues found among Web Applications. On one side, it's a "feature" since it allows web application users to correctly identify if their username exists on the platform, helping with remembering their username if they forgot it. On the other side, it's a "bug" since it allows attackers to use a dictionary of words to correctly identify registered usernames on the platform and subsequently perform a brute-force or password picking attack.

Login Screen

Say your Web Application has a login screen, when user tries to login with invalid username, and the web app tells the user "invalid username" instead of a bit more vague "invalid credentials", the user then knows that the username they just supplied doesn't exist on the platform.

login example

Forgot Password

When user creates a forgot password request and they supply invalid email, the application may respond "Email doesn't exist" or "If the supplied email exists, we've sent the forgot password link to it". The latter doesn't let the user know about the existence of supplied email on the platform while the former does.

First-Stop-Health

Resolution

The solution for CWE-204 is simple: changing the messaging for login and forgot password forms to more vague, non-disclosing statements:

"Username doesn't exist" -> "Invalid credentials."

"Email doesn't exist" -> "If the email exists, the forgot password email was sent"

"Forgot email sent" -> "If the email exists, the forgot password email was sent"

"Sorry, there was a problem sending the email" -> "If the email exists, the forgot password email was sent"

When messaging is sufficiently vague, an outsider won't be able to tell if username they just supplied exists on the platform.

Discrepancy in Timing

Even if your web application displays sufficiently vague messages for an outsider not to know the difference when supplying valid or invalid usernames. Discrepancy may be observed from the timing of the response.

Some of the forgot password forms incorporate API call or SMTP connection synchronously with the page load behavior. A common forgot password logic looks like this:

if user.exists
    mail.send()

"mail.send()" function takes a considerable delay to complete and holds up the page from loading. An outsider can observe this delay for valid emails because of the if statement in the code. Invalid usernames wouldn't trigger the "mail.send()" function and there would be no delay.

Resolution

To resolve the discrepancy in timing, follow best practices and make mail.send() function asynchronous: that executes independently of the page load process. If the function is asynchronous, it wouldn't delay the page load and outsider won't see the discrepancy in responses to valid and invalid usernames.

Python

Python has a great task queue tool called celery that could be used to run async tasks, like sending an email. Celery needs a queue broker, that could be redis, rabbitMQ, AmazonSQS, etc... You would need to install both the broker and celery as part of python packages before being able to use them. After that, create a task:

from celery import shared_task
from django.core.mail import send_mail
from datetime import datetime, timedelta

import uuid
@shared_task(bind=True)
def send_forgot_password(self, user):
    mail_subject="Forgot password for {user.email}"
    guid = uuid.uuid4()
    user.password_reset_uuid = guid
    user.password_reset_expiry = datetime.now() + timedelta(hours=2)
    user.save()
    link = "https://app.domain.com/password-reset/{guid}"
    message="Please follow the link below to reset your password:\n\r" + link
    to_email=user.email
    send_mail(
        subject= mail_subject,
        message=message,
        from_email='no-reply@domain.com'
        recipient_list=[to_email],
        fail_silently=True,
    )
    return "Done"

The task will be called from the view with .delay() function that creates a separate process through the celery queue management service to execute the function above:

# views.py
from django.shortcuts import render
from .forms import ForgotPasswordForm
from tasks import send_forgot_password
# Import additional necessary modules
def forgot_password_view(request):
    if request.method == 'POST':
        form = ForgotPasswordForm(request.POST)
        if form.is_valid():
            email = form.cleaned_data['email']
            send_forgot_password.delay()
            message = "If the provided emails exists, you will receive an email with a link to reset your password."
            return render(request, 'password_reset_done.html', {'message': message})
    else:
        form = ForgotPasswordForm()
return render(request, 'forgot_password.html', {'form': form})