While hunting on a Synack Red Team (SRT) engagement, I uncovered a critical business logic flaw that turned a standard checkout into a free-for-all. By exploiting a desync between the payment gateway and the shopping cart, I could finalize large orders for the price of a single, cheap item. The final amount appeared to be calculated earlier in the workflow and was not revalidated when the transaction was finalized.
In this blog, I’ll explain how I analyzed the transaction workflow, identified the logic gap, and exploited it to manipulate the final purchase.
Understanding the Typical Checkout Process
Before I explain the business logic issue, I’ll walk through a normal transaction workflow. In this case, the application followed a common transaction process where a user adds items to a shopping cart, proceeds to checkout, and is redirected to a payment page to complete the transaction through a payment gateway. Once the payment is successfully processed, the application finalizes the order and confirms the purchase.
In the normal transaction workflow shown here, the order data remains consistent throughout the entire transaction process. The items being paid for are the same items that will be ordered once payment is successful.
Identifying Synchronization Errors in Multi-Tab Checkout Sessions
While testing the transaction workflow, I noticed something unusual related to how transaction data was displayed and updated across the application. After adding an item to the shopping cart and proceeding to checkout, the application redirected to the payment page, where the final price was displayed. At this stage, I hold the payment page and open the shopping cart in another tab.
From there, I added additional items to the shopping cart. After updating the shopping cart, I refreshed the payment page, and the displayed amount remained unchanged, still reflecting the original transaction amount before the shopping cart updated. However, when viewing the shopping cart details, the newly added items and updated total were clearly visible.
This inconsistency indicated that the payment page was not synchronized with the shopping cart’s current state. More importantly, it suggested that the payment amount might be calculated earlier in the process and not revalidated when the transaction is finalized. At this point, I knew the payment process might still rely on outdated transaction data even after the shopping cart had changed.
Exploiting Stale Data to Manipulate Transaction Totals
The attack scenario involved starting a transaction with a cheap item and reaching the payment page. Instead of completing the payment immediately, I held the payment process and added additional items to the shopping cart in another tab. The payment page did not update its price after the modification, and the transaction could still be completed using the original amount before the cart was modified.
Once the payment was successfully processed, the application finalized the order using the updated shopping cart, even though the payment amount corresponded only to the original cart value.
As a result, I could purchase multiple items while only paying for the initial item in the shopping cart.
Preventing Business Logic Flaws in Payments
The root of the issue is that the payment process was not tightly bound to the transaction’s final state. There are several ways to prevent this type of vulnerability, including:
- Generate a finalized transaction snapshot: The system should generate a server-side snapshot of the transaction data, including the items, quantities, and total price. The payment request should reference this snapshot rather than the live shopping cart to ensure the order data cannot change during the payment process.
- Implement shopping cart versioning: Modification to the shopping cart should update a version identifier. The payment session should be tied to a specific cart version, and if the cart changes after the payment process begins, the transaction should be invalidated and require the user to restart the order process.
- Validate transaction data during payment confirmation: The application should perform server-side validation by comparing the charged amount to the finalized order data before finalizing the order. If any mismatch is detected between the payment amount and the order contents, the transaction should be rejected.
- Validate payment gateway callbacks: The application should verify that the charged amount matches the amount in the stored transaction snapshot. The application should not simply process the latest shopping cart state without validating the price and order details.
Business logic vulnerabilities like this one can be particularly dangerous because they don’t rely on traditional technical flaws, such as injection or authentication bypasses. In this case, the problem stemmed from how the transaction state was handled during the payment process.
The payment amount was determined when the user first reached the payment page. However, the application continued to allow changes to the shopping cart while that payment session was still active. Because the system never verified that the charged amount matched the final cart contents, the order could contain items that were never actually paid for.
Issues like this highlight why transaction workflows should treat order data as immutable once the payment process begins. Any mismatch between the charged amount and the finalized order should always be validated on the server before the purchase is confirmed.
Thanks for reading. Be sure to follow Synack and the Synack Red Team on LinkedIn for upcoming blogs in this series.