DEV Community

Cover image for Rate Limiting , DDOS & Captcha
Jayant
Jayant

Posted on

Rate Limiting , DDOS & Captcha

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}`);
});
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

Applying Rate-limiting

To Prevent this from happening we need to apply the rate-limiting to our server

  1. In-House Rate-limiting Logic
    we can rate the user by creating an Object that store the IP address with the no. of requests by the users in the past 60seconds.
    APPROACH -

    1. Gets the user IP Address from the request object.
    2. Store user IP Address with count of the request.
    3. Check if the user's ip exceeds the number of requests
    4. Early return if user is requesting too much.
  2. Using Express rate limit package

npm i express-rate-limit
Enter fullscreen mode Exit fullscreen mode

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
});
Enter fullscreen mode Exit fullscreen mode

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
DDOS Attack

To Prevent DDOS Attack

  1. use Captcha
  2. Instead of IP Blocking do blocking user based of their id for some time.

Captcha

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

  1. Create a React Project
  2. Go to Cloudflare > Search for Turnstile
  3. Add a New Site to the turnstile

Cloudflare Turnstile

  1. Keep your site key & secret key safe
  2. 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
Enter fullscreen mode Exit fullscreen mode
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;
Enter fullscreen mode Exit fullscreen mode

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" });
    }
});
Enter fullscreen mode Exit fullscreen mode

DDOS Protection in Production

  • Move your site to cloudflare
  • It will proxy all your requests through cloudflare server.

Cloudflare

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

Top comments (1)

Collapse
 
jay818 profile image
Jayant

Hope you liked it.