Welcome to another blog! Today, I’ll explain how you can effectively prevent OTP bypass attacks in your application. While I’ll focus on Node.js and React.js, the same concepts can be applied in other languages and frameworks too.
Let’s dive into the techniques and best practices to secure your OTP implementation and ensure your application stays safe from such vulnerabilities.
What is OTP Bypass?
OTP (One-Time Password) bypass refers to exploiting vulnerabilities in an application to log in or gain unauthorized access without providing a valid OTP. Attackers may use invalid OTPs, expired OTPs, or manipulate API responses to bypass the OTP verification mechanism.
One of the most commonly used tools for such attacks is Burp Suite. With this tool, attackers can intercept and modify API requests and responses. For example:
A valid response can be captured from a legitimate user.
This response is then copied and used to replace the invalid response of another user by intercepting the request.
This manipulation allows attackers to bypass OTP verification, even if the OTP is incorrect or expired.
For more details on securing your application click here to know more
how to stop preventing OTP Bypass through Response Manipulation
To fix this, you should implement encryption and decryption because users or hackers can read the response and payload if they are in plain text. It is better to secure the application with encryption (you can use AES or RSA)
What if the user gets the encryption keys? Can they still bypass it?
Yes, they can still bypass the encryption. The response is the same for all users. For example, if the frontend is set to allow login when it receives a 200 status code and the message 'OTP verified successfully,' it will still be vulnerable, as the response will be the same for another user. So, what can we do?
To address this, we need to keep a record for each user, so each response is unique and valid only for that specific user. For other users, the response would be invalid.
How can we achieve this without using a database?
Let's start by coding on the client side:
First, we will encrypt the payload
Then, we will generate a 7-character UID (you can use a string of any length).
After generating the UID, we will send it in the headers with the name 'rsid.'
Call the API.
Validate the response.
The main part: Check if the 'rsid' sent to the backend matches the one sent from the client. If they match, the login is successful; otherwise, it fails
const OnSubmit = async () => {
//encryption function to encrypt data
let data = await AesEncrypt(form);
let verifyobj = {
"encdata":data
}
// calling makeid function
let getid = await makeid(7);
//sending rsid to in headers
let config = {
headers: {
"rsid": getid,
}
}
//calling api
let ApiCallverify = await axios.post("http://localhost:4000/api/verifyotp",verifyobj,config);
//decrypting data from an api
let decryptedData = await Aesdecrypt(ApiCallverify.data.dataenc);
//verifying data
if(ApiCallverify && ApiCallverify.data.dataenc && ApiCallverify.status === 200)
{
//checking the rsid matching with frontend and backend
if(decryptedData.rsid === getid)
{
//success
alert(decryptedData.message)
}
else{
//fail
alert("Invaild User")
}
}
else{
//fail
alert(decryptedData.message)
}
}
Now, let's dive into the backend.
First, we validate the request body and check if it is encrypted and if it contains the rsid in the headers. If it matches all the requirements, we move on to the next steps; otherwise, we will send a response to the client indicating invalid data.
If everything matches, we decrypt the data and check if the received payload contains all the required fields after decryption. If it does, we validate whether the OTP and the user are valid or not (I used a static example here for illustration).
If everything matches, we send the encrypted response along with the rsid that we received from the client.
// POST /verify route
app.post("/api/verifyotp", async (req, res) => {
//checking if data is proper or not
if(req.body && req.body.encdata && req.headers['rsid'])
{
//valid payload
//decrypting payload
let decryptjson = await decryptData(req.body.encdata)
req.body = decryptjson;
const { phonenumber, otp } = req.body;
// Validate input
if (!phonenumber || !otp) {
return res.status(400).json({ error: "Phone and OTP are required" });
}
//verifying otp ( i used static creds to show example you can use db )
if(otp == 1234 && phonenumber == "12334567890")
{
//sending rsid that we recevied from client through headers and then encrypting data
let data= await AesEncrypt({ message: "Verification successful",rsid:req.headers['rsid']})
//sending response to client
return res.status(200).json({dataenc:data});
}
else{
//sending rsid that we recevied from client through headers and then encrypting data
let data= await AesEncrypt({ message: "Verification failed",rsid:req.headers['rsid']})
//sending response to client
return res.status(200).json({dataenc:data});
}
}
else{
//if we didn't recevied vaild data from client
return res.status(400).json({ error: "invaild data" });
}
});
Now, let's see the final output in the browser:
- Valid user
the headers
success
- Now, we will test for a failed or invalid user who copies the response of another user. Using Burp Suite, we will intercept the response. In this case, I'll just add a static rsid in the backend.
Finally, we have stopped OTP bypass through response manipulation.
I hope you liked my blog. Please leave a like!
Top comments (0)