Friday 11 December 2020

Timing attacks on login

In designing an authentication server one of the attacks we try to defend against are timing attacks.

One area where timing attacks can be used is to find a discrepancy factor in the login verification code to distinguish between non-existing user accounts and accounts that do exist but for which a wrong password is provided.

The OWASP Authentication Cheatsheet provides examples as well as a hint on how to deal with this, however, just using a secure password checker might not be enough as illustrated in the code shown below:

The checkpassword() function in our code uses a safe digest compare algorithm but the additional activities we perform for successfully authenticated users (illustrated by the placeholder comments) is more than for non-authenticated users and could be used to mount a timing attack.

with timing_equalizer(1.0):
    if user:
        logger.info(f"valid user found {user.email}")
        if checkpassword(req.params['password'], user.password):
            # delete old sessions
            # create new session
            # set session cookie
        else:
            logger.info(f'user authentication failed for known user {user.email}')
    else:
        logger.info(f'user authentication failed for unknown user {email}')

Solution

The simple approach I chose to mitigate those timing attacks is to enclose the code in a context manager that makes sure that all code takes at least a given amount of time, no matter which path through the code is chosen.

This is shown in the first line where we call the timing_equalizer context manager with an argument that tells it to make sure executing the context takes at least one second.

This slows down the code but logins happen infrequently and a 1 second delay is considered acceptable (and if the activities for an authenticated user could be proven to be faster, we could even lower this time). Also note that this does not limit the speed overall: different users logging in parallel are served by separate threads or processes and will each have their own delay.

The code for the context manager is rather straight forward:


from time import sleep, time
from contextlib import contextmanager
from loguru import logger

@contextmanager
def timing_equalizer(delta):
    """
    Make sure that code in context takes
    at least `delta` seconds.
    """
    mark = time() + delta
    try:
        yield mark
    finally:
        wait = mark - time()
        logger.info(f'waiting for {wait} seconds')
        if wait > 0:
            sleep(wait)

This context manager might also be used for other code that might reveal a discrepancy. for example the forgot password and register new account (where there is check if an account is already in use).