When Milliseconds Move Money: Exploiting a Race Condition in a Financial Transfer Workflow

A researcher testing a banking app’s Pay Card feature found that identical payment requests sent in parallel could push a 2,000-unit source account to -8,000 units while crediting a card with 10,000 units. The validation worked on single requests but not under concurrency. This post walks through the test, the impact, and the fix.

A line illustration of a yellow-on-blue bug with nodes reminiscent of a motherboard spreading out from it like legs

Technical Summary

Exploits Explained

Severity High
CVSS 7.5
Affected Components: Pay Card workflow card-payment requests source account balance validation account state updates

A race condition in a card-payment workflow allowed concurrent requests to push the source account below zero while increasing the destination card balance beyond the available funds. During testing, the source account contained only 2,000 units. After a group of identical payment requests was sent concurrently, the credit card balance increased by 10,000 units while the source account dropped to -8,000 units.

The resulting state was reflected immediately and persisted after the test. Customer-identifying information and the original currency have been removed.

Server-Side Validation That Failed Under Concurrency

I was testing the application feature by feature, focusing on input validation because financial applications contain many numeric fields tied to business rules. One feature was Pay Card, which allowed a user to fund their own credit card from their main account.

The flow was simple: main account → credit card → amount → confirmation → payment. Payments above the available source balance were rejected by both the UI and backend.

That raised another question: what would happen if several individually valid requests reached the backend at the same time?

Pay Card workflow before testing, with the source account limited to 2,000 units
Figure 1. The Pay Card workflow before testing, with the source account intentionally limited to 2,000 units.
Pay Card payment rejected with the message Insufficient Balance in payment account
Figure 2. A normal payment exceeding the available balance is rejected with “Insufficient Balance in payment account.”

Building the Race With Burp Suite Repeater

I intentionally kept the source balance at 2,000 units. One 2,000-unit payment should succeed; anything beyond that should fail.

I intercepted a valid Pay Card request using Burp Suite Community, sent it to Repeater, duplicated it into a large request group and used Send group in parallel. Every request used the same source account, destination card and 2,000-unit amount. No custom script was required.

Identical Pay Card requests grouped in Burp Suite Community Repeater for parallel execution
Figure 3. Identical Pay Card requests grouped in Burp Suite Community and prepared for parallel execution.

Normal flow: Check available balance → process payment → update account state.

Race condition: Multiple valid requests → concurrent processing → inconsistent state → payments beyond available balance.

From black-box testing alone, I could not determine the exact backend implementation responsible for the behavior. However, the observed result was consistent with a race between balance validation and the subsequent state update, where multiple concurrent requests were able to act on the same available balance before the system reached a consistent final state. The precise cause, such as transaction handling, locking, or isolation behavior, could not be confirmed without backend visibility.

The Money Came From Nowhere

After the parallel requests were completed, the credit card balance increased from 230,353 units to 240,353 units. At the same time, the source account changed from 2,000 units to -8,000 units.

Only 2,000 units had existed in the source account, but the destination card had been credited with 10,000 units. The workflow had therefore processed 8,000 units beyond the available funds.

Once the negative balance appeared and the corresponding increase was visible on the card, I stopped testing, collected evidence and reported the finding. I did not attempt to spend or withdraw the additional balance.

One accepted parallel payment response from the Pay Card endpoint
Figure 4. One accepted parallel payment response
Dashboard after the race condition showing the source account at -8,000 units and the card at 240,353 units
Figure 5. The dashboard after the race condition, showing the source account at -8,000 units and the card balance increased by 10,000 units (240,353 units).

More Than a Display Bug

The result was not simply a frontend inconsistency. The updated balances were immediately reflected by the application and remained persistent. The increased credit card balance was also consistently reflected anywhere the card balance was displayed throughout the application.

I returned to the Pay Card functionality after the source account had reached -8,000 units and attempted another payment for 2,000 units. The application responded with “Insufficient Balance in payment account.”

Pay Card attempt rejected after the source account entered a negative balance
Figure 6. A subsequent Pay Card attempt is rejected after the source account has entered the negative state.

This provided an additional indication that the negative balance was not simply a temporary display issue. The application was using the resulting negative source balance when making subsequent financial decisions, while the increased credit card balance remained consistently reflected across the application.

The Score and the Impact

Synack scored the finding 7.5, High, with the CVSS vector AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N. The functionality was available to a standard application user through the normal Pay Card workflow.

The primary impact was integrity: a user could credit a card beyond the funds available in the source account. I stopped before testing whether the additional balance could be spent or withdrawn, leaving that impact for the customer to validate safely.

Fixing Race Conditions With Transactions and Locking

The exact backend changes were not disclosed. Financial state changes should not depend on validation rules alone. Balance validation and state updates should be coordinated transactionally so concurrent requests cannot act on stale account state. Depending on the architecture, this may involve database transactions, locking, isolation controls and server-side consistency checks.

After remediation, I repeated the concurrency test with another standard test account and different values. Parallel requests were rejected, balance integrity was preserved and the issue could no longer be reproduced.

The Lesson: Validation Is Not Concurrency Safety

Server-side validation does not automatically make a business rule safe under concurrency. An amount field can reject values above the available balance and still become exploitable when several valid requests are processed at nearly the same time.

The tooling was not the interesting part. I used Burp Suite Community and a standard Repeater feature. The key question was not only, “Does the backend validate this value?” but also, “What happens if it validates the same state several times at once?”

A few milliseconds were enough to turn 2,000 available units into a 10,000-unit card credit and a -8,000 source balance. Sometimes the vulnerability is not inside the request, it’s in the timing between requests.

Frequently Asked Questions

What would 1,500
elite hackers find in
your stack?

The Synack Red Team and Sara AI Pentesting work side by side to test your environment and find vulnerabilities that matter.

See Synack In Action

Learn how the Synack Platform can secure your organization