395 words
2 minutes
Uncovering a Race Condition in the Transfer of Memberships.

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 account

But 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:

AccountInitial StateFinal State
Account_A1 membership (paid)0 memberships
Account_B0 memberships2 memberships (unpaid)
Account_C0 memberships2 memberships (unpaid)
Account_D0 memberships2 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:

  1. 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.

  1. 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.

  1. 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.