First estimate what would it cost you or your users if smb. has really has got access to the data of some of your users. Will this user lose 1 000 000 USD or will it just lose a few emails inviting to the local bowling club? Then estimate of somebody will really want to invest time and other resources to break your system. If there is no sense for an attacker, then your approach may be quite sufficient.
But if you really want to prevent attacks, let's consider following. Here is just one possible attack scenario.
An attacker registers as a user on your web site or finds somebody who is already registered and wants to help, this doesn't matter.
The attacker determines the time shift on your server with high precision, like several seconds.
The attacker chooses some attack time. For a time period around this time the attacker calculates the possible reset tokens for every microsecond. In the period of plus-minus 1 minute there are 120 000 000 microseconds. For every value the attacker calculates the password reset tokens. The MD5 is very fast, so it doesn't take much time. Also for storing of 120 000 000 hashes not much space is needed.
Then at the chosen time the attacker sends consequently multiple password reset requests to your server. For simplicity, let say 5 requests with the attacker user name, then one requests with the target user name, then again 5 requests with the attacker user name. Then the attacker receives 10 reset emails from your server. Based on these tokens and on the per-calculated hash list, the attacker knows exactly the time stamps used by your server for every reset token.
Of course the requests can be processed asynchronously. A request sent later can be processed a few milliseconds earlier. But with a big number of requests this gives a high precision.
It can be even worse, if your server has some patterns in timestamps. For instance, your server might have resource bottlenecks in token generation, and the real time stamps might be generated not with a step of 0.000001 s, but might be a multiplier of let say 0.020 s. This might greatly reduce the number of possible timestamps to test.
Now the attacker program tries to reset password for every token, starting from the most probable ones. The total number of attempts depends on your server. In the worst case the attacker may need to test 1 000 - 10 000 tokens. In the good case even a few requests may be sufficient.
It depends also on how you implement password reset. If you lock the user after 3-5 failed password reset attempts, this may prevent successful attack. But there are still 2 potential problems:
- The precision of determining of the target user timestamp may be very high. Then even 1-2 attempts might be sufficient.
- The attacker can have the database of all user names and can request password reset for all of them or for many of them at the same time. Then even if you lock users after 3-5 failed attempts, the attacker has better chance to find a user or several users for whom the timestamp will be determined precisely and thus the reset will succeed.
The security of hashes depends essentially on entropy, or simply put, on randomness. If the values are predictable, this makes such hashes vulnerable to attacks. The function
random_bytes uses a cryptographic random number generator, which uses much more entropy and makes generated values practically unpredictable.