If your application includes fundamental features like login, registration, password reset/recovery, resend confirmation links, and other specific functionalities requiring server requests, it's crucial to implement mechanisms against brute force attacks and the generation of a substantial load on your service. Without such mechanisms, your application may be vulnerable to various threats, including sending an excessive number of emails/OTPs to users, potentially leading to financial and reputational damage.
Many web applications lack adequate rate-limiting measures, relying solely on the limitations imposed by their business logic, such as restricting the number of requests based on a payment model. Some applications, however, do incorporate rate limits, particularly for operations like login attempts, registration, and other critical functionalities. These implementations often depend on the X-Forwarded-For header for IP address tracking.
To illustrate a simple example, I came up with this code snippet on Flask
from flask import Flask, request, jsonify
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
app = Flask(__name__)
limiter = Limiter(
app,
key_func=get_remote_address,
storage_uri="memory://",)
def get_ipaddr():
# Retrieve the client's IP address from the request
# X-Forwarded-For header is used to handle requests behind a proxy
ip_address = request.headers.get('X-Forwarded-For', request.remote_addr)
return ip_address
# Rate limit to 5 requests per minute per IP
@limiter.limit("5 per minute")
@app.route('/')
def index():
ip_address = get_ipaddr()
return ip_address
In the following sections, I'll explain various approaches to testing and attempting to bypass rate limits in your application.
To efficiently test your app for this type of vulnerability, automation is a powerful tool. You can achieve this by employing scripts, such as those in Python (as I often do), or using tools like Burp Suite (btw a great tool for testers and cybersecurity professionals). Also, tools like Postman might be used to automate checks relatively easily.
X-Originating-IP: 127.0.0.1
Use different IP values in every request you send.
Use double X-Forwared-For header.
X-Forwarded-For:
X-Forwarded-For: 127.0.0.1
Try the same with different headers.
X-Originating-IP: 127.0.0.1
X-Remote-IP: 127.0.0.1
X-Remote-Addr: 127.0.0.1
X-Client-IP: 127.0.0.1
X-Host: 127.0.0.1
X-Forwared-Host: 127.0.0.1
Try changing the user-agent, content-type, accept-language, etc, or cookies, anything that could be used as a user identifier.
If there is a rate limit of 3 tries per IP, every three tries, change the IP value of the header (or other headers or params in your requests).
Try adding to the params you send
%00, %0d%0a, %0d, %0a, %09, %0C, %20
For example
param1=value1%%0d%0a
param2=value2%00
For example, if you are requesting OTP for email verification and you only have 3 tries, use the 3 tries for
[email protected]
[email protected]%00
[email protected]%0d%0a
And so on
If you are testing, for example,/API/v1/signup endpoint, then try to perform brute-force to /Signup, /SignUp, /sign-up. Try adding blank chars (from the above) to the original endpoints.
If the limit is on the requests of /api/v1/resetpassword endpoint, try brute-forcing it by adding some query params - once the rate limit is reached, try, for example, /api/v1/resetpassword?param1=value1
It might be the case that an app has flawed logic - if you log into your account before each attempt/series of attempts, the rate limit is reset for your IP, and you can continue your password brute-force attack. If you are testing a login feature, you can do this in Burp Suit with a Pitchfork attack in the settings (or you can write your own script for this) for each attempt/series of attempts.
Here is my example of how I automated a simple check for the X-Forwarded-For header just to get a POW:
from random import randint
import requests
import json
url = "https://yourapp.net/api/v1/regconfirm-resend"
data = {
"email": "[email protected]"
}
N = 100
def generate_random_ip():
return '.'.join(
str(randint(0, 255)) for _ in range(4)
)
for _ in range(N):
headers = {
"Host": "yourapp.net",
"Content-Type": "application/json",
"X-Forwarded-For": generate_random_ip()
}
response = requests.post(url, headers=headers, data=json.dumps(data))
print(headers)
print(f"Status Code: {response.status_code}, Response: {response.text}")
A possible solution might be the use of Cloudflare and its mechanisms. A detailed explanation can be found here restoring-original-visitor-ips. I will provide only a brief overview of its defense mechanisms.
If you use applications that depend on the incoming IP address of the original visitor, a Cloudflare IP address is logged by default. The original visitor IP address appears in an appended HTTP header called CF-Connecting-IP. By following our web server instructions, you can log the original visitor IP address at your origin server. If this HTTP header is not available when requests reach your origin server, check your Transform Rules and Managed Transforms configuration.
If Pseudo IPv4 is set to Overwrite Headers - Cloudflare overwrites the existing Cf-Connecting-IP and X-Forwarded-For headers with a pseudo IPv4 address while preserving the real IPv6 address in CF-Connecting-IPv6 header.
NOTE: Remember, when implementing such a defense mechanism, perform thorough testing. This approach might be subsequently applied to the entire cluster and could adversely affect certain functions and microservices where it is unnecessary. In a nutshell, while fixing a security breach, be cautious, as it could potentially impact the entire application. Analyze which part of your system might be affected negatively and test everything before shipping changes to the prod env.
Also published here.