Security Tip: Compare keys with hash_equals()

[Tip#56] It may be tempting to compare keys/sensitive strings using `===`, or even `==`, but that opens you up to timing attacks! You should be using a timing attack safe string comparison function...

Security Tip: Compare keys with hash_equals()

Greetings friends! I hope you enjoyed last week’s 2 years of Securing Laravel recap - it was great to take a look at how much we’ve covered in the past 12 months. This week I want to remind you about one of PHP’s security helper functions, which you should be using within your apps to protect against timing attacks. I’ve mentioned it a couple of times before1, but we haven’t specifically featured it yet, .

Before we get started with the Security Tip, I just wanted to mention that for personal reasons I’ll be reducing my hours next year for my Laravel Security Audits and Penetration Tests. Please reach out if you’re thinking about an audit, so I can reserve you time in my schedule2. 🕵️



Compare keys with hash_equals()

It may be tempting to compare keys, or other sensitive strings, using the strict comparison `===` (or even the loose comparison `==`) however this opens up to timing attacks, which can be used to identify the keys you’re comparing.

But first, what I mean by “keys” or “sensitive strings”?

These are string values that the user does not know but needs to provide in order to do or access something. Common examples are API keys, passwords, tokens, secret phrases, answers, etc… Some apps will use these a lot, while others might not ever need them.

Consider this code:

// api.secret_token => 'V1cHt2S67DADJIm9sX9yzCc272EkSC'

if ($request->token === config('api.secret_token')) {
    // Do something...
}

In this example, the user needs to provide a valid token before they are allowed to do something. However, since the strict comparison in use (`===`) is not timing attack safe, an attacker can eventually figure out the value by providing specific values of `$request->token` and measuring the timing of the response3.

The solution is simple, all you need to do is to use `hash_equals()` to compare the two strings! This ensures the comparison always takes exactly the same amount of time regardless of the inputted string4, preventing an attacker from figuring out the secret key:

// api.secret_token => 'V1cHt2S67DADJIm9sX9yzCc272EkSC'

if (hash_equals(config('api.secret_token'), $request->token)) {
    // Do something...
}

Since it’s a simple function that replaces your strict (or loose) comparison, it’s trivial to implement, so there really is no reason not to use it. 🙂

You can learn more about `hash_equals()` in the PHP docs: https://www.php.net/manual/en/function.hash-equals.php.

If you want to learn more about timing attacks, go check out In Depth #6: Timing Attacks, where we covered them in detail, including a section about string comparisons which directly relates to this issue. You should check out Anthony Ferrara’s “It’s All About Time” article to dive deeper into timing attacks.


Looking to learn more?
Security Tip #37: New Password Generator
▶️ In Depth #13: Stealing Password Tokens with Forwarded Host Poisoning

  1. We talked about it in: In Depth #9: Signed URLs, In Depth #10: Magic Emails, and In Depth #6: Timing Attacks.

  2. I will be booking in past clients first, and then I would love to give Securing Laravel subscribers second preference before opening up to new clients.

  3. It may take a lot of requests, and a simple rate limiter can make this significantly harder, but it’s still a very real problem that you need to take seriously.

  4. Ok, it’s only “exactly the same” when both values are the same length. However, figuring out the length isn’t overly helpful if you can’t also figure out what it contains.