OTP replay: “One time” doesn’t always mean one session
It was an ordinary insurance claim-submitting web application. The authentication process was simple. Provide a username and password, followed by entering an OTP (one-time password or passcode) as part of a two-factor authentication flow. To control user access to the system, we received a client request to disallow certain user domains from accessing the application.
To test this, a user logs into the system with a username and password. Upon valid credential entry, an OTP will be generated and sent to the email address. I entered the OTP, and the dashboard was loaded successfully.
The change requirement
The client's requirement was to block certain email domains (e.g., Gmail, Yahoo) from accessing the application. This should prevent users from those domains from logging in or registering on the application. Since this will impact the login and registration functionalities for several user roles, and we need to test across multiple browsers, I decided to validate using two or more screens instead of going back and forth between them. I loaded the app in three browsers. I entered the URL, followed by the valid credentials and the OTP code, and the login was successful.
The red flag
Something made me pause! This was when I noticed something strange. I had entered the exact same OTP code three times across different browser sessions. Now that is a critical red alert!
What I had just encountered was an OTP Replay Attack. This is a serious vulnerability. In any secure system, an OTP should be one-time. Once the OTP is verified, it should be invalidated immediately. This OTP replay vulnerability happens when the system fails to delete and destroy the OTP token, allowing an attacker to reuse the same code to authorize a system or a transaction.
What is an OTP?
An OTP, or one-time password, is a secure, unique code sent to your device or email address for identity verification during the login process. This adds an extra layer of security to the login process. A typical OTP code should expire after one use, or it can remain valid for 30 seconds to several minutes before the first use.
In a secure system, the OTP lifecycle is straightforward.
- Code Generation - The system generates a secure single-use code
- Code delivery - The generated code is delivered to the user via a secure channel such as email, SMS or an authenticator app
- Code verification - The system validates the user-submitted code against the active token
- Code expiry - The generated code is invalidated upon successful use, timeout or maximum attempts. The system should instantly flag that specific code as ‘used’ or delete it from the cache database.
Any subsequent attempt to reuse the exact same code should be met with a 401 unauthorized or a 400 Bad Request error.
The bug!
In this case, the code did not expire after the first use. I was still able to use it across multiple browsers and even devices, which leaves the door open for unauthorised access. In our critical scenario, the logical trap was that the system had checked three parameters on the second and third attempts.
- Are the entered username and password valid and matching?
- Does the entered OTP match the code stored in the cache?
- Has the OTP expiration time elapsed?
An OTP expiration time is typically 3 to 5 minutes. So I decided to wait for about 10 minutes after the previous code generation before making another login attempt. This time, it gave me a fresh new code. A code completely different from the previous one. So what happened here was that the generated OTP remained active until the expiration time had elapsed, even after it had already been used. The developer has forgotten the most vital rule of Multi-Factor Authentication (MFA). The verification event must immediately trigger the OTP token's absolute end, regardless of how much time remains in the token's lifespan.
An attacker still needs the user’s password. Without that, just the OTP is not a big deal. An OTP is an essential defence wall of any organisation. They rely on it to mitigate the risk of password leaks, credential stuffing, and shoulder surfing. But when an OTP is replayed, that defensive wall is shattered.
Imagine a person-in-the-middle (PitM) or on-path attack intercepting the valid OTP along with the credentials while a user logs into a system. In our case, the OTP does not become invalid once it is used. This opens a golden window for the attacker to execute a script that replays the exact same OTP on a different machine. Now that the attacker has gained access, he can easily manipulate claim payouts, steal Personally Identifiable Information (PII) or even alter banking details. The user who is actively browsing on the other side of the window is completely unaware that their window has been cloned.
Finding this bug taught me that we, as testers, should not rely only on the ‘happy paths’. If I had tested only the straightforward functionality, this critical flaw would have made it to production. It was only when I paused and thought, ‘How was I able to reuse the code and log in?’ that I then found the bug.
The following are some of the test approaches for you to follow next time you come across OTP testing:
Use the same OTP:
- On multiple browser sessions
- Multiple devices (Desktop, mobile, tablet, etc.).
The OTP is only valid:
- One browser session or one device.
- Before the code's validity period expires.
- For the account issued for.
- If it hasn’t been used, even if the user uses the back button.
Conclusion
With more and more new technologies, software systems are now highly interconnected. We testers make numerous assumptions while testing, and this is our greatest vulnerability. The technical specification might just say, “Upon valid credentials, the input system should send a one-time passcode”. While looking at the implementation, we may easily conclude that the testing passes, as the implementation simply matches the literal definition of the words. What the specification doesn’t say is just as critical.
- The code should be deleted immediately following the successful use,
- Stop reuse of the code, and time out after a certain period of time.
When such implicit requirements are left out, the system will be exposed.
That afternoon test I performed proved that we should not always believe what we see. The phrase ‘One time’ means only one successful verification. We are the guardians of a stable and secure system outcome. Therefore, we should have the ability to go beyond that happy path, break the ordinary authentication lifecycle, and take necessary actions to protect user data from any exploitation. Look not just for bugs but also for risks and vulnerabilities.