3

Understanding timing attacks with code examples

 2 years ago
source link: https://dev.to/propelauth/understanding-timing-attacks-with-code-examples-32e6
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Vulnerable login example

The following code snippet has a subtle security issue with it. Can you tell what's wrong?

// Returns true if the email/password pair is valid
async function isValidCredentials(emailAddress, password) {
    // Fetch the password hash from the DB by email address
    const passwordHashOrNull = await fetchPasswordHash(emailAddress);

    // If there was no match, return false
    if (!passwordHashOrNull) {
        return false;
    }

    // Bcrypt is "a library to help you hash passwords"
    // Here we use the compare function to check that the
    //   provided password matches the hashed password in the DB
    const doesPasswordMatch = await bcrypt.compare(password, passwordHashOrNull);
    return doesPasswordMatch;
}

// Fetches the password hash from the DB
async function fetchPasswordHash(emailAddress) {
    // impl not important
}
Enter fullscreen modeExit fullscreen mode

As a hint, let's look at how long a few calls to isValidCredentials takes:

async function timeIsValidCredentials(emailAddress, password) {
    console.time("Checking " + emailAddress);
    await isValidCredentials(emailAddress, password);
    console.timeEnd("Checking " + emailAddress);
}

await timeIsValidCredentials("[email protected]", "password");
// Checking [email protected]: 63.813ms
await timeIsValidCredentials("[email protected]", "password2");
// Checking [email protected]: 62.867ms
await timeIsValidCredentials("[email protected]", "password");
// Checking [email protected]: 4.017ms
await timeIsValidCredentials("[email protected]", "password");
// Checking [email protected]: 4.008ms
Enter fullscreen modeExit fullscreen mode

There's a noticeable difference between how long [email protected] emails take and [email protected] or [email protected].

It turns out that the issue is these lines:

  // If there was no match, return false
if (!passwordHashOrNull) {
    return false;
}
Enter fullscreen modeExit fullscreen mode

By returning early if there was no match, an attacker can easily tell that [email protected] has an account, but [email protected] and [email protected] don't.

Timing attacks

This is a common example of a timing attack. They are a class of attacks where the length of time that your application takes to perform a task leaks some information.

In the login case, the difference in times made it pretty obvious from even one request. If the difference was more subtle, an attacker can make many requests over a long time and average them together to distinguish different cases.

Is it a big deal?

This might not seem like a big deal, but let's say I'm trying to find someone's personal email. I only have their name, and I know they have signed up for your site.

I can try a bunch of variations of [email protected] or lastname{3digitnumber}@gmail.com and so on until I find a valid one.

Additionally, there are other timing attacks that leak even more sensitive information, which we'll see in a bit.

How can we fix it?

There are a few strategies, but the simplest answer is "make sure all codepaths take the same amount of time". You don't have to do this everywhere, just in sensitive parts of the codebase.

Instead of returning early, we could have checked the password against some hash and then returned false:

// If there was no match, waste time and then return false
if (!passwordHashOrNull) {
    await bcrypt.compare(password, RANDOM_PASSWORD_HASH);
    return false;
}
Enter fullscreen modeExit fullscreen mode

It is also useful to add rate limiting whenever possible. If an attacker needs a lot of requests to distinguish different cases, rate limiting them could make the attack impractical.

Timing attacks in practice

Recently, a clever timing attack was found in Lobste.rs' password reset. It exploited the fact that databases when comparing two strings will return early if the strings don't match.

So checking

"a".repeat(10000) === "b".repeat(10000)
Enter fullscreen modeExit fullscreen mode

should take less time than

"a".repeat(10000) === "a".repeat(9999) + "b"
Enter fullscreen modeExit fullscreen mode

This means that the more characters you have correct, the longer the call will take. An attacker could try different prefixes and see which one takes the longest to slowly determine a valid password reset token.

This same vulnerability exists anywhere where someone is checking a secret value directly against a database, so while it may seem pretty theoretical, there are definitely real world cases that have been reported and fixed.


Recommend

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK