Good Carder
Professional
- Messages
- 938
- Reaction score
- 566
- Points
- 93
From a carder to carders. Most people think that hacking a payment gateway means finding an SQL injection or forging a JWT token. But there's a more elegant way that doesn't require complex reverse engineering or zero-day exploits. Simply send two requests at the right time, and the system will automatically reward you with the product.
In this article, I'll explore how race conditions in payment gateways allow you to get free goods, double your crypto, and charge funds multiple times. You'll learn about race conditions, real-world vulnerabilities in Stripe and Braintree, and receive ready-made scripts for testing.
A classic problem is "read-modify-write": two requests simultaneously check the balance, both see that there are sufficient funds, and only then both debit the funds. As a result, the user spends more than they originally had. Two requests simultaneously check the order status, both see "pending," and both confirm the payment. As a result, the customer pays twice, and the seller receives double commission or, conversely, gives the item away for free. Two concurrent transactions attempt to use the same one-time coupon or promo code, and the system activates it twice.
For a carder, a race condition is a gold mine. No need to hack cryptography, no need to steal API keys. Simply send multiple requests at the right time with minimal latency. According to research by Indie Hackers, a race condition in Stripe webhooks caused users to spontaneously pay twice, and support tickets took weeks to resolve. In the WordPress plugin Woo Wallet, one user spent 9.20 on a balance of 2.70 simply by sending five concurrent checkout requests. These vulnerabilities don't require fancy payload or expensive tools — just the ability to count milliseconds.
A real-world example: the Woo Wallet plugin for WordPress had a critical vulnerability that allowed users to spend more funds than they had. The vulnerable code looked like this: first, the get_wallet_balance() function was called, then an if ( $amount > $balance ) check was performed, and only then the debit was written. Since there was no lock between the check and the write, multiple concurrent requests managed to read the same balance. In the developer's logs, four debit transactions occurred within one second: they all showed a "post-transaction balance" of 0.40, because the initial balance of 2.70 was read concurrently. As a result, one user spent 9.20 with only 2.70.
In another project, mpp-rs (a Rust implementation of a multi-party payment system), carders could forge or replay requests to the tempo/charge endpoint without ever completing a real Stripe payment, because the server did not check whether a valid payment intent existed before updating the credit balance.
A similar attack is possible with one-time coupons or bonuses. When sending multiple requests to activate the same coupon concurrently, the system can apply it multiple times because each request sees that the coupon hasn't yet been used. Stripe once encountered a similar situation: a carder sent 30 requests to activate a discount offer, and the system applied the discount 30 times, turning 20,000 into 600,000 fee-free processing.
In one Stripe integration, the webhook was handled idempotently, but due to the lack of an atomic "insert if not exists" in the database and the use of non-atomic WordPress metadata, two concurrent webhooks for the same event were still processed. The result: double subscription crediting.
Another case: payment_intent.succeeded, charge.succeeded, and invoice.paid were arriving out of sync, and the code blindly updated the subscription status with the latest of them — as a result, the subscription could end up in an erroneous state.
The issue could also be on the merchant side, where one endpoint confirms the payment and another activates access. If requests are sent to both at the same time, the user could receive the service without actually paying. In mpp-rs, a flaw in state synchronization between Stripe and the in-memory storage allowed an unlimited number of paid sessions to be created without being charged, simply by faking the userId in the request.
In 2026, Turbo Intruder remains the standard. Stripe admitted through HackerOne that a carder used Burp to simultaneously activate a discount: he sent 30 requests simultaneously, and the system applied the discount 30 times.
Step-by-step PoC:
The ampersand runs commands in parallel in the background, creating child processes that run concurrently. This is how, in one case, a carder sent five order requests and spent 11.50 with a balance of 5.
If the system isn't protected by idempotency, three threads could confirm the same session_id, creating duplicate transactions with the same Stripe payment intent. This is how the Stripe WordPress plugin worked: retries after a timeout created duplicate payments because the idempotency key was regenerated for each call rather than saved for retries.
Stripe provides an idempotency mechanism: you must pass the Idempotency-Key header. Stripe guarantees that requests with the same key will be processed only once. Integration errors (for example, when the key is regenerated for each retrieval instead of being saved and reused) lead to duplicate payments. Idempotency should be system-wide, not just the external API.
If zero rows are affected, there are insufficient funds.
In the WordPress Woo Wallet plugin, the atomic command would look like this:
With parallel requests, only one of them will update the row, the rest will see that the balance has already been reduced and will not proceed.
Then do an atomic insert with ON CONFLICT:
If the insertion succeeds, we process the request. If not, we return the saved result.
Event ordering is also important: monitor Stripe timestamps and apply updates only if the new event is fresh.
If the version doesn't match, it means another concurrent query has already updated the record. The query returns 0 affected rows, and the handler realizes it has lost the race.
The patch works like this: the server checks for Idempotent-Replayed and, if present, refuses to process the request again. Without this check, a carder can pay once and then resend the same payment token indefinitely until the merchant's resources are exhausted.
A quick one-line reminder:
"A race condition isn't a bug, it's a feature if you can count milliseconds. Double-spend is born from two parallel UPDATEs, multi-endpoint from asynchronously adding a shopping cart, webhooks are duplicated upon timeout. Turbo Intruder catches race conditions in seconds, Burp Parallel batches requests. An atomic UPDATE with the balance >= amount condition kills double-spend, and a queue with an order key breaks webhooks. Stripe provides idempotency, but stores ignore it. Your profit lies in someone else's carelessness."
In this article, I'll explore how race conditions in payment gateways allow you to get free goods, double your crypto, and charge funds multiple times. You'll learn about race conditions, real-world vulnerabilities in Stripe and Braintree, and receive ready-made scripts for testing.
Part 1. The Race the Carder Wins
A race condition is a situation where two or more concurrent requests attempt to modify the same resource simultaneously, and the system is unable to correctly process the order of operations. In payment systems, this leads to balance checks and debits not occurring atomically.A classic problem is "read-modify-write": two requests simultaneously check the balance, both see that there are sufficient funds, and only then both debit the funds. As a result, the user spends more than they originally had. Two requests simultaneously check the order status, both see "pending," and both confirm the payment. As a result, the customer pays twice, and the seller receives double commission or, conversely, gives the item away for free. Two concurrent transactions attempt to use the same one-time coupon or promo code, and the system activates it twice.
For a carder, a race condition is a gold mine. No need to hack cryptography, no need to steal API keys. Simply send multiple requests at the right time with minimal latency. According to research by Indie Hackers, a race condition in Stripe webhooks caused users to spontaneously pay twice, and support tickets took weeks to resolve. In the WordPress plugin Woo Wallet, one user spent 9.20 on a balance of 2.70 simply by sending five concurrent checkout requests. These vulnerabilities don't require fancy payload or expensive tools — just the ability to count milliseconds.
Part 2. Race Condition Types and Real-World Examples
2.1. Double-spend: How to spend more than you have
The most common type: A user with a limited balance submits two or more debit requests simultaneously. The system checks the balance for each request using the same initial value, after which all requests are successful, and the balance goes into negative balance.A real-world example: the Woo Wallet plugin for WordPress had a critical vulnerability that allowed users to spend more funds than they had. The vulnerable code looked like this: first, the get_wallet_balance() function was called, then an if ( $amount > $balance ) check was performed, and only then the debit was written. Since there was no lock between the check and the write, multiple concurrent requests managed to read the same balance. In the developer's logs, four debit transactions occurred within one second: they all showed a "post-transaction balance" of 0.40, because the initial balance of 2.70 was read concurrently. As a result, one user spent 9.20 with only 2.70.
In another project, mpp-rs (a Rust implementation of a multi-party payment system), carders could forge or replay requests to the tempo/charge endpoint without ever completing a real Stripe payment, because the server did not check whether a valid payment intent existed before updating the credit balance.
2.2. Multi-endpoint race condition: when an attack occurs on two fronts
A more sophisticated attack occurs when two different API endpoints process related operations out of sync. For example, one endpoint adds an item to the cart, while the other confirms checkout. If you send a request to add an expensive item simultaneously with a checkout request (which doesn't yet contain this item), you can pull off the following trick: initially, the cart contains only a Gift Card. Two requests are sent simultaneously — one to checkout the current cart and one to add the expensive item. The checkout request sees that the total doesn't exceed the balance, and the addition of the item updates the cart after checkout is confirmed. As a result, the expensive item is "sticked" to an already paid order and is sold for free.A similar attack is possible with one-time coupons or bonuses. When sending multiple requests to activate the same coupon concurrently, the system can apply it multiple times because each request sees that the coupon hasn't yet been used. Stripe once encountered a similar situation: a carder sent 30 requests to activate a discount offer, and the system applied the discount 30 times, turning 20,000 into 600,000 fee-free processing.
2.3. Race Conditions in Webhooks: When Stripe Is Its Own Enemy
Stripe sends webhooks at least once and retries them if a timeout occurs — this is standard behavior. However, if the webhook handler isn't idempotent, retriggering the same event will result in a duplicate charge or credit. A classic problem: two events arrive almost simultaneously, both see the order as pending, and both confirm the payment. The customer pays twice, and the merchant is shocked. An investigation into the incident revealed that Stripe, by default, doesn't consider a webhook successfully delivered until it receives a 200 OK response. If your handler takes longer than 300 ms, Stripe retry the webhook, and unless duplicate protection is implemented, the customer receives a second charge.In one Stripe integration, the webhook was handled idempotently, but due to the lack of an atomic "insert if not exists" in the database and the use of non-atomic WordPress metadata, two concurrent webhooks for the same event were still processed. The result: double subscription crediting.
Another case: payment_intent.succeeded, charge.succeeded, and invoice.paid were arriving out of sync, and the code blindly updated the subscription status with the latest of them — as a result, the subscription could end up in an erroneous state.
The issue could also be on the merchant side, where one endpoint confirms the payment and another activates access. If requests are sent to both at the same time, the user could receive the service without actually paying. In mpp-rs, a flaw in state synchronization between Stripe and the in-memory storage allowed an unlimited number of paid sessions to be created without being charged, simply by faking the userId in the request.
Part 3. Toolkit: How to Detect and Exploit
Exploiting race conditions requires tools that allow sending parallel requests with precise timing.3.1. Burp Suite Turbo Intruder
Python script inside Burp for high-speed parallel sending:
Python:
def queueRequests(target, wordlists):
engine = RequestEngine(endpoint=target.endpoint,
concurrentConnections=50,
requestsPerConnection=100,
pipeline=False)
for i in range(50):
engine.queue(target.req, i)
engine.start()
In 2026, Turbo Intruder remains the standard. Stripe admitted through HackerOne that a carder used Burp to simultaneously activate a discount: he sent 30 requests simultaneously, and the system applied the discount 30 times.
3.2. Parallel group в Burp Repeater
You can manually group two requests and select the "Send group (parallel)" mode. This creates a delay of several milliseconds between sending packets, which causes a race condition.Step-by-step PoC:
- Log in with a balance of $100.
- Add Gift Card to cart.
- We intercept the request to add an expensive item to the cart and the request to confirm the order in Burp Repeater.
- We leave only the Gift Card in the cart.
- Set the group sending method: “Send group (parallel)”.
- We launch a parallel sending: a request to add an expensive item and a request to confirm the cart are sent simultaneously.
- Expensive items are "sticked" to an already paid order, and the balance is debited only for the Gift Card.
3.3 Python script with threading
Python:
import requests
import threading
def purchase(url, payload):
r = requests.post(url, json=payload)
print(f"Status: {r.status_code}")
url = "https://target.com/checkout"
payload = {"product_id": 1, "quantity": 1}
threads = []
for i in range(10):
t = threading.Thread(target=purchase, args=(url, payload))
threads.append(t)
t.start()
for t in threads:
t.join()
3.4 Asynchronous mode in cURL
Bash:
for i in {1..10}; do curl -X POST https://target.com/api/charge -d '{"amount":100}' & done
The ampersand runs commands in parallel in the background, creating child processes that run concurrently. This is how, in one case, a carder sent five order requests and spent 11.50 with a balance of 5.
3.5. Stripe Checkout Test Script (PoC)
Python:
import requests
import threading
def complete_checkout(session_id, payload):
r = requests.post(f"https://target.com/complete/{session_id}", json=payload)
print(f"Completed: {session_id}")
session_id = "cs_test_123"
payload = {"payment_method_id": "pm_123"}
threads = []
for i in range(3):
t = threading.Thread(target=complete_checkout, args=(session_id, payload))
threads.append(t)
t.start()
If the system isn't protected by idempotency, three threads could confirm the same session_id, creating duplicate transactions with the same Stripe payment intent. This is how the Stripe WordPress plugin worked: retries after a timeout created duplicate payments because the idempotency key was regenerated for each call rather than saved for retries.
Part 4. Race Condition Protection
If you're a carder developing a payment gateway integration, please review the following information:4.1 Idempotent API Keys
Atomic insertion ensures that of two concurrent requests with the same key, only one will succeed, and the second will return the saved result.Stripe provides an idempotency mechanism: you must pass the Idempotency-Key header. Stripe guarantees that requests with the same key will be processed only once. Integration errors (for example, when the key is regenerated for each retrieval instead of being saved and reused) lead to duplicate payments. Idempotency should be system-wide, not just the external API.
4.2. Atomic updates in the database
Instead of reading the balance into memory, then checking and updating, do everything with one atomic SQL command:
SQL:
UPDATE wallets SET balance = balance - ? WHERE user_id = ? AND balance >= ?;
If zero rows are affected, there are insufficient funds.
In the WordPress Woo Wallet plugin, the atomic command would look like this:
SQL:
UPDATE wallets SET balance = balance - 2.30 WHERE user_id = X AND balance >= 2.30;
With parallel requests, only one of them will update the row, the rest will see that the balance has already been reduced and will not proceed.
4.3 Unique index on idempotency_key
The idempotency table must be stored in the database:
SQL:
CREATE TABLE idempotency_keys (
key TEXT PRIMARY KEY,
response JSONB,
created_at TIMESTAMP DEFAULT NOW()
);
Then do an atomic insert with ON CONFLICT:
SQL:
INSERT INTO idempotency_keys (key) VALUES ($1) ON CONFLICT (key) DO NOTHING;
If the insertion succeeds, we process the request. If not, we return the saved result.
4.4. Single worker + webhook processing queue
Separation of synchronous confirmation and asynchronous business logic. The webhook only needs to verify the event and queue the task. A single worker processes the tasks sequentially. This eliminates race conditions because only one transaction per object is processed at a time.Event ordering is also important: monitor Stripe timestamps and apply updates only if the new event is fresh.
4.5. Optimistic Locking
Add a version field to the orders table. When updating, check that the version hasn't changed:
SQL:
UPDATE orders SET status = 'paid', version = version + 1 WHERE id = ? AND version = ?;
If the version doesn't match, it means another concurrent query has already updated the record. The query returns 0 affected rows, and the handler realizes it has lost the race.
4.6. Queues with a partitioning key
The guarantee of a single writer per Stripe object: a queue keyed by event.data.object.id ensures that tasks for the same order are never processed in parallel. If one worker is already running, the other simply waits. Even if Stripe sends two duplicate webhooks simultaneously, they won't cause double writing.4.7. Checking the Idempotent-Replayed Header
Stripe can send the Idempotent-Replayed header — a direct indication that a previous request with the same key has already been processed and the response is being replayed from the cache. Ignoring this header resulted in funds being credited again without an actual payment.The patch works like this: the server checks for Idempotent-Replayed and, if present, refuses to process the request again. Without this check, a carder can pay once and then resend the same payment token indefinitely until the merchant's resources are exhausted.
4.8. Speed Limit
Even without perfect synchronization, user-level rate limiting can break the chain of parallel requests. If a user sends more than five requests per second to the payment endpoint, blocking them for a minute won't solve the problem, but it will make mass exploitation more difficult.4.9. Three-level reliability model
The model described above includes three components: an optimistic state transition in the database, an idempotent key for the external API, and a background job for reconciliation with Stripe. Even if the webhook handler crashes after setting status = 'processing', the job will be able to query Stripe using the idempotent key and move the order to the correct state. Without such a backup, orders could remain stuck in processing forever.Summary
Race conditions in payment gateways aren't a freak of fate, but a systemic problem that can be systematically exploited. Double-spend via parallel requests, multi-endpoint attacks for the free shopping cart, idempotency at the database level, and webhook queues are your weapons. Stripe and Braintree are protected at the API level, but their clients (stores, plugins) almost always have their own vulnerabilities. No matter how complex payment cryptography, the atomicity of database operations is still violated by trivial thread races.A quick one-line reminder:
"A race condition isn't a bug, it's a feature if you can count milliseconds. Double-spend is born from two parallel UPDATEs, multi-endpoint from asynchronously adding a shopping cart, webhooks are duplicated upon timeout. Turbo Intruder catches race conditions in seconds, Burp Parallel batches requests. An atomic UPDATE with the balance >= amount condition kills double-spend, and a queue with an order key breaks webhooks. Stripe provides idempotency, but stores ignore it. Your profit lies in someone else's carelessness."