August 14, 2026
I Changed ₹500 to ₹1 — How I Found a Payment Logic Bug in a QR Code
The QR Code Looked Normal. The Payment Logic Wasn’t.

By Cyber Tamarin
2 min read
While testing target.com, I came across an interesting payment workflow.
Everything initially looked normal.
I added an item to the cart, proceeded through checkout, and reached the payment page. The application generated a UPI QR code containing the payment information.
At this point, I started wondering:
What exactly is inside this QR code?
I extracted the QR payload and inspected the parameters.
One parameter immediately caught my attention:
am=500am=500am appeared to represent the payment amount.
The order was worth ₹500, so this made sense.
But then came the question every bug hunter eventually asks:
What happens if I change it?
The One-Character Test
I modified the amount from:
am=500am=500to:
am=1am=1Nothing complicated.
No exploit chain. No sophisticated payload. Just changing the payment amount.
I then used the modified QR code with a UPI application and made a payment of ₹1.
At this point, I expected the transaction to fail.
Instead...
The payment went through.
The application reported the payment as successful.
That was the moment the vulnerability became interesting.
₹500 Order. ₹1 Payment.
The original order value was significantly higher than ₹1.
Yet the application accepted the ₹1 transaction as a successful payment.
This suggested that the backend was checking something along the lines of:
Payment received?
↓
YES
↓
Mark order as paidPayment received?
↓
YES
↓
Mark order as paidInstead of enforcing:
Expected amount = ₹500
Paid amount = ₹1
↓
MISMATCH
↓
Reject paymentExpected amount = ₹500
Paid amount = ₹1
↓
MISMATCH
↓
Reject paymentThat distinction is extremely important in payment systems.
A payment being successful does not necessarily mean the correct payment was made.
Why This Is Dangerous
At first glance, changing a QR parameter might look like a small client-side manipulation.
It isn't.
The important question is what happens after the manipulation.
If an attacker can modify the amount and the backend accepts the resulting payment without independently validating the expected amount, the attacker can potentially purchase goods or services for an artificially reduced price.
For example:
Actual Order Value: ₹1,000
Modified QR:
am=1
Amount Paid:
₹1
Application:
Payment SuccessfulActual Order Value: ₹1,000
Modified QR:
am=1
Amount Paid:
₹1
Application:
Payment SuccessfulThat's no longer just parameter tampering.
That's a payment integrity failure.
The Root Cause
The core issue was trusting payment information that could be influenced by the client.
The QR code should not be treated as the source of truth for the amount owed.
The server already knows:
Order ID → ₹1,000Order ID → ₹1,000Therefore, after receiving a payment confirmation, the backend should independently verify the transaction:
Order Amount: ₹1,000
Gateway Amount: ₹1
Result:
❌ Amount mismatch
❌ Order remains unpaidOrder Amount: ₹1,000
Gateway Amount: ₹1
Result:
❌ Amount mismatch
❌ Order remains unpaidOnly when the values match should the application complete the order:
Order Amount: ₹1,000
Gateway Amount: ₹1,000
Result:
✓ Payment verified
✓ Order marked as paidOrder Amount: ₹1,000
Gateway Amount: ₹1,000
Result:
✓ Payment verified
✓ Order marked as paidWhat Should Be Fixed?
The most important fix is simple:
Never trust the payment amount supplied by the client.
The backend should:
- Retrieve the expected amount directly from the database.
- Generate the payment transaction using that server-side amount.
- Associate the payment with a unique order/transaction ID.
- Verify the payment directly with the payment gateway.
- Compare the gateway-confirmed amount against the expected order amount.
- Reject transactions where the amounts don't match.
- Only mark the order as paid after successful server-side reconciliation.
Cryptographic signatures can also be used to protect payment payload integrity, but signatures should complement—not replace—server-side payment verification.
The Takeaway
The lesson here is simple:
If the client can change it, test whether the server actually cares.
A single parameter like am=500 looked harmless, but changing it to am=1 exposed a serious payment logic flaw.
You don't always need a complex exploit. Sometimes, one parameter and one question — "Does the backend validate this?" — is enough to find the bug.
Question the assumptions. That's where the interesting bugs hide.
Thanks for reading.
Cyber Tamarin out.🍌