Rate Limiting , DDOS & Captcha
Jayant
Posted on August 22, 2024
Rate Limiting
It basically means, limiting the amount of request a user can sent to an endpoint.
Rate Limiting can be applied to every endpoint, depending on the endpoint usecase, we can loose or tighten up the amount of allowed request/time.
For Example - For a /api/v1/reset-password
endpoint, we should allow less request/time. Let say 5 request per minute.
Rate Limiting either can happen at Load Balancer level or application level
How it helps
- Protects the server from brute-force attacks.
- Make sure the server remains available for all. As a single person can't flood the server with requests and prevent anyone from using that service.
- Prevent Server crashes and help to manage the load on the server.
Basic Example of a Brute-force Attack.
Suppose this is the code that we are running on our server.
It has
/generate-otp
/reset-password
endpoints.
import express from "express";
const app = express();
const PORT = 3000;
app.use(express.json());
// In-memory Object to store the OTP
const otpStore: Record<string, string> = {};
app.post("/generate-otp", (req, res) => {
const email = req.body.email;
if (!email) {
return res.status(400).json({
message: "Email is required",
});
}
const otp = Math.floor(Math.random() * 10000 + 1).toString();
otpStore[email] = otp;
console.log(`OTP for ${email} is ${otp}`);
res.status(200).json({
message: "OTP Sent",
});
});
app.post("/reset-password", (req, res) => {
const email = req.body.email;
const otp = req.body.otp;
const password = req.body.password;
if (!email || !otp || !password) {
return res.status(400).json({
message: "Email, OTP and Password are required",
});
}
if (otpStore[email] !== otp) {
return res.status(400).json({
message: "Invalid OTP",
});
}
console.log(
`Password reset for ${email} with OTP ${otp} and new password ${password}`
);
delete otpStore[email];
res.status(200).json({
message: "Password Reset",
});
});
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});
we can use a For Loop
to do brute force attack
and reset user's password
import axios from "axios";
async function BruteAttack(otp: string) {
let data = JSON.stringify({
email: "yadavjayant@gmail.com",
otp,
password: "123456",
});
let config = {
method: "post",
maxBodyLength: Infinity,
url: "http://localhost:3000/reset-password",
headers: {
"Content-Type": "application/json",
},
data: data,
};
await axios.request(config);
}
async function main() {
for (let i = 0; i < 99999; i += 100) {
console.log(i);
let promises = [];
for (let j = 0; j < 100; j++) {
// as the BruteAttack function is async so it returns a promise so we need to await for it, but we can't await for so many request, A better approach might be batching lets send 100 requests and wait for them to finish then send next 100 requests.
// this can be achieved using Promise.all
promises.push(BruteAttack((i + j).toString()));
}
try {
// It will give error if any one of the 100 promise throw any error, it will only be resolved if all the promises are resolved.
await Promise.all(promises);
} catch (e) {}
}
}
main();
Applying Rate-limiting
To Prevent this from happening we need to apply the rate-limiting to our server
-
In-House Rate-limiting Logic
we can rate the user by creating an Object that store theIP
address with the no. of requests by the users in the past 60seconds.
APPROACH -- Gets the user IP Address from the request object.
- Store user IP Address with count of the request.
- Check if the user's ip exceeds the number of requests
- Early return if user is requesting too much.
Using Express rate limit package
npm i express-rate-limit
Add the rate Limit Configuration to the Code
const passwordResetLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // Limit each IP to 5 password reset requests per windowMs
message:
"Too many password reset attempts, please try again after 15 minutes",
standardHeaders: true,
legacyHeaders: false,
});
app.post("/reset-password", passwordResetLimiter, (req, res) => {
// Rest of your code
});
The Above is for a specific endpoint, if you want to apply the rate limit to all the endpoints then use app.use()
.
DDOS
DDOS means Distributed Denial of Services
. It happens when bunch of systems makes request to your server and thereby choking your server.So that your server remain unresponsive to all the other request and ultimately crashes.
This can be done by using virus or ransomware
To Prevent DDOS Attack
- use Captcha
- Instead of IP Blocking do blocking user based of their id for some time.
Captcha
Captcha make sure that the request is made by a human not a robot or machine.
To Add Captchas to your project you can use Clouflare Turnstile
- Create a React Project
- Go to Cloudflare > Search for Turnstile
- Add a New Site to the turnstile
- Keep your site key & secret key safe
- To use Captcha's in React we use a react-library that encapsulates a lots of logic for us react-turnstile Install it in the react-app
npm i @marsidev/react-turnstile
import { Turnstile } from "@marsidev/react-turnstile";
import "./App.css";
import axios from "axios";
import { useState } from "react";
function App() {
const [token, setToken] = useState < string > "";
return (
<>
<input placeholder="OTP"></input>
<input placeholder="New password"></input>
{/*
while solving captcha our site is talking to a cloudflare worker if the captcha is solved a token is generated and send to the server.
*/}
<Turnstile
onSuccess={(token) => {
setToken(token);
}}
// No need to hide your site_key
siteKey="YOUR_SITE_KEY"
/>
<button
onClick={() => {
axios.post("http://localhost:3000/reset-password", {
email: "yadavjayant2003@gmail.com",
otp: "123456",
token: token,
});
}}
>
Update password
</button>
</>
);
}
export default App;
Update your Backend Code a bit.
// Endpoint to reset password
app.post("/reset-password", async (req, res) => {
const { email, otp, newPassword, token } = req.body;
console.log(token);
// cloudflare expects the formData
// we need to reverify it on server cuz the user can explicitly send the request.
// Token can only be used once.
let formData = new FormData();
formData.append("secret", SECRET_KEY);
formData.append("response", token);
const url = "https://challenges.cloudflare.com/turnstile/v0/siteverify";
const result = await fetch(url, {
body: formData,
method: "POST",
});
const challengeSucceeded = (await result.json()).success;
if (!challengeSucceeded) {
return res.status(403).json({ message: "Invalid reCAPTCHA token" });
}
if (!email || !otp || !newPassword) {
return res
.status(400)
.json({ message: "Email, OTP, and new password are required" });
}
if (Number(otpStore[email]) === Number(otp)) {
console.log(`Password for ${email} has been reset to: ${newPassword}`);
delete otpStore[email]; // Clear the OTP after use
res.status(200).json({ message: "Password has been reset successfully" });
} else {
res.status(401).json({ message: "Invalid OTP" });
}
});
DDOS Protection in Production
- Move your site to cloudflare
- It will proxy all your requests through cloudflare server.
Cloudflare acts as an mediator.
You can also block Specific IP's or do the Region based blocking.
Other Options - aws shield
.
Thanks For Reading the Blog
Posted on August 22, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.