
From payment processing to membership transfers: How I uncovered another systemic concurrency issue.
Introduction
A few days ago I wrote about a race condition discovery in a payment processing flow. Still in the same target, in noticed something interesting in the public repo:
# TODO: █████████ small potential for exploit if someone can make █████████████████.So, they clearly knew about it, but the impact I found was much bigger than they anticipated.
The Architecture Problem
Here’s what was happening under the hood (without exposing implementation specifics):
User initiates transfer → INSERT to destination account → UPDATE source accountBut neither operation was wrapped in a database transaction. Each executed independently with autocommit enabled, creating a window for exploitation.
The Exploit Scenario
To demonstrate the vulnerability without causing real damage, I set up four test accounts:
| Account | Initial State | Final State |
|---|---|---|
| Account_A | 1 membership (paid) | 0 memberships |
| Account_B | 0 memberships | 2 memberships (unpaid) |
| Account_C | 0 memberships | 2 memberships (unpaid) |
| Account_D | 0 memberships | 2 memberships (unpaid) |
One payment, six memberships generated. This gave an attacker the possibility of buying one membership, send/multiply that membership into several accounts, and sell those accounts at a reduced price.
The Technical Details
Three structural weaknesses created this vulnerability:
- No Explicit Transactions
Each database operation ran as its own atomic unit under autocommit. Without START TRANSACTION / COMMIT wrapping, there was no guarantee that related operations would execute together.
- Missing Unique Constraints
The memberships table lacked a unique constraint preventing duplicate records. The auto-increment primary key silently accepted every insert, regardless of logical duplicates.
- No Pessimistic Locking
Without SELECT … FOR UPDATE, there was no mechanism to block concurrent reads. Multiple requests could simultaneously read the same source membership and all attempt to transfer it.
Aftermatch
The team quickly patched the vulnerability and asked me to test it, as well as helping them to check whether it had been exploited by anyone else. Fortunately, my test accounts were the only ones that had taken advantage of the vulnerability.
Lessons Learned
Concurrency bugs cluster together. Once you find one race condition in a system, look for others. They tend to hide in the same architectural weak points.
Developer comments can be breadcrumbs. Public TODO notes acknowledging vulnerabilities are essentially invitations to investigate. Treat them as signposts pointing to suspicious code.
Post-disclosure engagement pays dividends. After my first bounty, I stayed engaged with the team. When I found the second issue, they responded faster because trust was already established.
Cleanup goodwill matters. Offering to help clean up test data signals you’re a partner, not an adversary. Those gestures build relationships that last beyond individual bounties.
