TL;DR: Once a user successfully provides a valid OTP, that OTP can be replayed for the duration of the timestep. Upgrade to version 2.0.0 or higher.
Devise-Two-Factor implements RFC 6238 which defines a way to provide what is commonly known as Two-Factor Authentication (2FA).
RFC 6238 states that once a valid OTP is successfully proven to the server, the server must reject all subsequent validation attempts of that OTP for a given timestep. In other words, as the name implies, the OTP can only be used once.
Devise-Two-Factor failed to "burn" a given OTP code once validated successfully.
Despite violating a security design concern in the RFC, the impact is quite small.
- Attacker has a window of opportunity of the timestep (30 seconds by default)
- Attacker must shoulder-surf or MiTM the OTP code.
- Attacker must already know the victim's password.
Satisfying these conditions will defeat two-factor authentication for the victim, in that one authentication scenario.
However, the Man-in-The-Middles (MiTM) scenario is moot since, if an attacker can MiTM the
connection, they can just obtain the granted session secret from the response instead.
Because the server (aka verifier) must now "remember" which tokens are valid and which are not, a storage mechanism must be used. Caches are a pallatable solution but permenant storage is desired.
In the case of Devise-Two-Factor, the verifier simply writes down the last successful OTP code. When a prover supplies an OTP, the verifier does two things:
- Checks if the given code is valid for the given timestep
- Checks if the given code is not the same as the previous successful OTP code
If both conditions pass then the user (prover) is valid and should be issued an authentication token.
The supplied code will never* match the previously stored value unless its two attempts in the same timestep (aka our replay attack).
*there is a tiny chance that a future timestep will produce the same code because of the pigeonhole principle. In which case the prover (user) will receive a false-positive error. Trying again in a T+1 timestep will resolve the issue. Because this probability is so slim (I think 1 in a million), it's considered "never to occur".
Specifically to Devise-Two-Factor, the patch is here: https://github.com/tinfoil/devise-two-factor/commit/cb025fbd7fd257a057dd82de50aead7fbc987e8f
Applicable to Other Libraries
It is extremely likely Devise-Two-Factor is not the only library implementing TOTPs incorrectly, failing to guard replayed OTP codes. Checking other libraries in various languages would likely expose the same exploit.
Thanks to Viliam Holub for originally reporting the issue.
Header image is by Brent Moore under the Creative Commons license CC BY-NC 2.0.