Unrestricted Resource Consumption in a Password Reset — Web API Security Champion Part IV

June 17, 2024

This article is a part of Web API Security Champion series focused on API security vulnerabilities presented in a practical manner.

Unrestricted Resource Consumption

Description

Unrestricted Resource Consumption is an API vulnerability that occurs when an API endpoint allows the consumption of system resources such as CPU, memory, disk space, or network bandwidth without proper limitations. This vulnerability can lead to denial-of-service (DoS) attacks, where an attacker exploits the lack of restrictions to overwhelm the system, rendering it unavailable to legitimate users.

Unrestricted Resource Consumption is #4 on the OWASP TOP 10 API Security Risks list.

Impact

An attacker can exploit Unrestricted Resource Consumption to exhaust system resources, causing the application to slow down, crash or increase its hosting costs. Depending on the resource targeted, this could lead to various severe consequences:

  • Denial of Service: The application or certain feature becomes unavailable to legitimate users.
  • Data corruption: Overloaded systems may fail to process data correctly.
  • Financial impact: Increased operational costs due to excessive resource usage.
  • Reputational damage: Loss of trust from users and customers due to downtime or poor performance.

A few examples of Unrestricted Resource Consumption vulnerabilities reported via HackerOne include:

Case Study — Damn Vulnerable RESTaurant API

Just like in the previous articles of the Web API Security Champion series, I will also use my FastAPI based open-source project — Damn Vulnerable RESTaurant API to present the vulnerability in a practical way.

Vulnerability Description

Unrestricted Resource Consumption represents a broad class of vulnerabilities in an application’s APIs. As mentioned in the introduction, it can affect various resources, not just CPU, RAM, or storage of the server.

I decided to present the vulnerability in a password reset feature which can be found in in the /reset-password API endpoint of the Damn Vulnerable RESTaurant. This endpoint initiates password reset flow by sending a verification code via text message to the user’s phone number after providing a username and a phone number by a user. Further in the process, the verification code can be used to set a new password via another API endpoint available at /reset-password/new-password.

Here is the vulnerable API endpoint implementation, which can be also found in /app/apis/auth/service.py file on GitHub:

@router.post(
    "/reset-password",
    status_code=status.HTTP_200_OK,
)
def reset_password(
    data: ResetPasswordData,
    db: Session = Depends(get_db),
):
    user = db.query(User).filter(User.username == data.username).first()
    if not user:
        raise HTTPException(
            status_code=400,
            detail="Invalid username or phone number",
        )
    if user.role != UserRole.CUSTOMER:
        raise HTTPException(
            status_code=400,
            detail="Only customers can reset their password through this feature",
        )

    if user.phone_number.replace(" ", "") != data.phone_number.replace(" ", ""):
        raise HTTPException(
            status_code=400,
            detail="Invalid username or phone number",
        )

    # 4 digits PIN code and 15 minutes expiration shouldn't be bypassed
    # right?
    user.reset_password_code = "".join([str(secrets.randbelow(10)) for _ in range(4)])
    user.reset_password_code_expiry_date = datetime.now() + timedelta(minutes=15)
    db.add(user)
    db.commit()

    send_code_to_phone_number(user.phone_number, user.reset_password_code)
    return {"detail": "PIN code sent to your phone number"}

There are two security vulnerabilities in this code but for the purposes of this article I will focus on a rate limiting issue as there are no limits on the number of reset password requests a user can initiate through /reset-password. It can lead to a potential abuse where an attacker could flood the system with requests, incurring significant costs due to the large volume of text messages sent via send_code_to_phone_number function.

To provide a cost analysis for text messaging services, I examined one of the most popular providers. As of this writing, the cost per text message is approximately $0.0079 for the US, $0.0463 for the UK, $0.0431 for Poland, and $0.0940 for Germany. This means that 1,000 password reset requests would cost over $40 for the aforementioned European countries and just $7.90 for the US.

A traffic load of 1,000 HTTP requests can be generated in just a few seconds from a single machine. Imagine the potential impact if a determined attacker could generate millions of such requests during an attack. The costs could be enormous!

It’s important to note that these prices reflect rates available to small or medium-sized companies without any negotiated discounts. Larger companies may negotiate significantly lower prices based on their usage. However, even with better pricing, the security implications remain significant.

Furthermore, there is also another critical vulnerability in this mechanism which can be exploited by sending a number of HTTP requests but I will leave this challenge for you. Can you identify it based on the above code and this endpoint?

Unrestricted Resource Consumption Proof of Concept

Exploiting this vulnerability can be done by sending multiple requests in a short period. For example, the following Python script can be used to flood the API with reset password requests:

import requests

url = 'http://localhost:8080/reset-password'
reset_data = {
    'username': 'existing_username',
    'phone_number': '1234567890'
}

for _ in range(1000):
    response = requests.post(url, json=reset_data)
    print(response.status_code, response.json())

This script sends 1,000 reset password requests in quick succession. Each request triggers the sending of a text message with a reset code, which can quickly lead to a large number of text messages being sent.

Unrestricted Resource Consumption Fix

Depending on the business requirements, you might allow initiating up to three password resets per 24 hours per user or per IP address. Personally, I recommend limiting password resets per user since a single IP address could be shared by many users. However, this approach might expose users to targeted denial-of-service (DoS) attacks, potentially blocking their ability to reset their passwords.

To mitigate this risk, it’s advisable to implement robust protections against automated request submissions, such as utilizing CAPTCHA solutions like reCAPTCHA or hCaptcha.

When implementing a rate-limiting solution, technologies like Redis or other in-memory storage solutions are often used due to their low-latency reads and writes. For FastAPI, there is an open-source solution called fastapi-limiter based on Redis. This solution is relatively easy to set up, allowing you to implement rate limiting for each endpoint and modify the user’s identifier.

An example of an IP-based rate limiting implementation for the password reset feature is shown below. To permit up to three password resets per 24 hours, you need to add the RateLimiter(times=3, hours=24) dependency to the router arguments for the API endpoint.

from fastapi_limiter.depends import RateLimiter

@router.post(
    "/reset-password",
    status_code=status.HTTP_200_OK,
    dependencies=[Depends(RateLimiter(times=3, hours=24))]
)
def reset_password(
    data: ResetPasswordData,
    db: Session = Depends(get_db),
):
user = db.query(User).filter(User.username == data.username).first()
    if not user:
        raise HTTPException(
            status_code=400,
            detail="Invalid username or phone number",
        )
    if user.role != UserRole.CUSTOMER:
        raise HTTPException(
            status_code=400,
            detail="Only customers can reset their password through this feature",
        )

    if user.phone_number.replace(" ", "") != data.phone_number.replace(" ", ""):
        raise HTTPException(
            status_code=400,
            detail="Invalid username or phone number",
        )

    code_length = 6
    user.reset_password_code = "".join([str(secrets.randbelow(10)) for _ in range(4)])
    user.reset_password_code_expiry_date = datetime.now() + timedelta(minutes=15)
    db.add(user)
    db.commit()

    send_code_to_phone_number(user.phone_number, user.reset_password_code)
    return {"detail": "PIN code sent to your phone number"}

Additionally, in the code shown above, I adjusted the length of the password reset code to 6 characters. I’d recommend this length from a security perspective as it provides a good balance between security and user experience (UX).

Unrestricted Resource Consumption Recommendations

To prevent vulnerabilities related with unrestricted resource consumption, I recommend the following:

  • Implement rate limiting: Use rate limiting to control the number of requests a user can make in a given time period.
  • Enforce quotas: Set quotas for resource usage, such as the number of messages sent or the amount of data processed.
  • Use resource limits: Apply resource limits at the system level, such as CPU and memory constraints for processes.
  • Monitor resource usage: Continuously monitor resource usage to detect and respond to potential abuse.
  • Write tests for resource limits: Ensure that resource limits are enforced through tests.
  • Perform code reviews: Review code to identify potential resource consumption issues and lack of rate limiting checks.
  • Perform threat modeling: Features and API endpoints potentially sensitive in context of resource consumption can be identified during threat modeling excercise.

An example of a simple unit test which ensures that a rate limiting is enforced for a password reset endpoint:

def test_reset_password_rate_limit_exceeded(test_db, client):
    url = '/reset-password'
    reset_data = {'username': 'testuser', 'phone_number': 

    for _ in range(3):
        response = client.post(url, json=reset_data)
        assert response.status_code == 200

    response = client.post(url, json=reset_data)
    assert response.status_code == 429

Summary

In this article, I presented the Unrestricted Resource Consumption vulnerability with practical example, fix, and recommendations for developers and security engineers. In my opinion, understanding and mitigating this vulnerability is crucial for building robust and resilient APIs. Finally, it’s important to note that each vulnerability of this class can vary, impact different resources, and require distinct mitigation strategies.

References

Interesting Article?

Join DevSec Selection!

DevSec Selection is a bi-weekly Newsletter with the latest outstanding articles related with DevSecOps and application security.


Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments