Security Tip: Do You Really Need a Hash for That?

[Tip#65] Before you reach for a hashing function, stop and think about what you're hashing and why you're hashing it...

Security Tip: Do You Really Need a Hash for That?

👉 Laravel Security Audit and Penetration Tests → I’m currently fully booked until late March, so if you’re thinking about an audit in Q1/Q2 2024, reach out now to reserve your slot! 🕵️


I frequently come across code that looks like this in my audits:

$model = Model::create(...);
$model->hash = md5($model->id);
$model->save();

return $model->hash;

A new model is created, and then a new hash is generated, based off some aspect of the model data. This is often done from the incremental ID itself, but it’s also common to use user submitted data like names, filenames, email addresses, etc, or even just a timestamp. This hash is almost always generated using `md5()` or `sha1()`, and sometimes a form of “randomness” added, such as with `time()` or `rand()`. The hash is then returned and given to the user in some way - usually via a filename or URL key.

It’s easy to see the problem the developer is trying to solve: How do I generate an unguessable unique key for this model?

I can see the appeal of using `md5()` or `sha1()` here. They are both incredibly fast and produce a short output which looks unique and appears unguessable, so it’s easy to reach for those functions in this situation. Especially if you’re not familiar with newer hashing algorithms, such as SHA256. However, if you’re passing the hash back to the user, they are horribly insecure1. In fact, even using a secure hashing algorithm like SHA256 on it’s own in this scenario should be avoided. If your inputs are predictable (in this case, an incremental ID), then the output can be easily brute-forced.

So what’s the solution? It depends…

Generating an unguessable unique token?

If you just need a unguessable unique token which is generated once and reused, without needing regeneration or verification to prevent changes, then there is no reason to hash anything. Instead, there are a few methods you can use to generate a secure unguessable unique random string:

  1. `Str::random(32)` → My personal favourite. It’s completely unguessable, and basically unique2.

  2. `Str::uuid()` → A popular choice. Generates a UUID (v4) that is unguessable and unique.

For special cases, there are some potentially less-secure3 options too:

  1. `Str::ulid()` → A short time-orderable unique token, which uses a microseconds and secure randomness to produce tokens that can be sorted without revealing sensitive information like an incremental ID.
  2. HashIDs → Masks incremental IDs behind a short hash, which can be decrypted to extract the original ID. These are useful if you can’t store the hash, but need to mask an ID.

Generating verifiable signatures or repeatable hashes?

This email is already far too long for a weekly tip, so I’m going to cover this next time!

If you’d like to keep learning about this and do some homework, then take a look at Hash-based Message Authentication Codes (HMAC), and PHP’s `hash_hmac()` function4. We’ve covered them before when we looked at Laravel’s Signed URLs


Looking to learn more?
Security Tip #46: Security Headers are Layers of Defence
▶️ In Depth #16: What Are Insecure Functions?

  1. MD5 and SHA1 are considered broken and should never be used in a secure context. They are both incredibly fast, making brute-force trivial, and collisions have been discovered in both, which compromises their outputs further.

  2. If you’re really worried about the unique aspect, you can add a unique constraint to your DB column, but the chances of a collision are virtually impossible.

  3. I’m calling these “potentially less-secure” because they include significantly less randomness (ULID) and/or are much shorter (HashIDs), which can make them easier to predict. This would still require significant effort to exploit though, so they would be fine in many cases where you just need an unidentifiable token. But they shouldn’t be relied upon to protect routes, etc.

  4. https://www.php.net/manual/en/function.hash-hmac.php