Hacking Into India’s Largest Payment Network Through a Single API Call
“What if I told you that with just one POST request, you could go from being a cashier to controlling thousands of retail stores across India?”
That’s exactly what I stumbled upon when poking around the self-registration feature of India’s biggest physical payment systems provider. In a time where fintech systems are evolving faster than ever, you’d assume access control is a solved problem. But what I found was a simple, silent bypass that let me impersonate an admin — at scale.
Setting the Stage
I was exploring the self-sign-up flow of this fintech giant’s merchant dashboard, which is widely used in electronics chains, retail stores, and massive showroom networks across the country. The idea is simple: merchants and employees register via a known TID (Terminal ID) and onboard themselves. The system then places them in the right role (usually a cashier) inside the corresponding store.
But this system had one fatal flaw: the role and store assignments were completely client-controlled.
Initial Recon
Once you enter the flow at [REDACTED], you’re prompted to:
- Enter your email and complete OTP verification.
- Provide a TID (Terminal ID) to associate yourself with a store. (They were kind enough to provide a sample TID in the help section itself)
- Select an account type — where only the “Cashier” role is freely selectable.
This sends a GraphQL mutation request to the backend, namely the UpdateUser operation.
Here’s the original request:
POST /gateway/graphql
Host: [SUBDOMAIN].[REDACTED].com
Content-Type: application/json
{
"operationName": "UpdateUser",
"variables": {
"input": {
"userType": "MERCHANT",
"name": { "firstName": "test", "lastName": "" },
"userMapping": {
"merchantId": "1600",
"isDefault": true,
"role": {
"id": "1537",
"name": "Store Cashier"
},
"stores": ["13793"],
"merchantName": "India's Biggest Electronic Store"
},
"source": "Sign-Up",
"contactDetails": [],
"onBoardingType": "Self"
}
},
"query": "mutation UpdateUser($input: UpdateUserInput!) { updateUser(input: $input) { isUserUpdated } }"
}At first glance, everything looked fine. I got access to the expected store, in the expected role.
But here’s the thing — nothing was server-side enforced.
Escalating Privileges
With the session live and my user authenticated, I resubmitted the same GraphQL mutation with a few changes:
- Changed the
role.idfrom1537(Cashier) to1536(Admin) — Scoured through the frontend static files to find all possible role values - Modified the
storesarray to include other store IDs (either random guesses or adjacent to the current one)
"role": {
"id": "1536",
"name": "Store Admin"
},
"stores": ["13790", "13791"]Boom.
After refreshing the dashboard, I was suddenly not just a cashier — but an admin for multiple stores, including some that had nothing to do with me.
I now had access to:
- Full employee and cashier lists
- Order and transaction history
- POS machine details
- Billing and invoice records
- EMI customer data
- Settlement reports
- …basically everything.
Going Nuclear: The Super Admin Shortcut
What happens if you send an empty store list?
"stores": []Shockingly, this doesn’t revoke access. Instead, it grants admin access to every store on the platform.
I had just escalated myself to super admin, without ever touching an internal endpoint or breaking authentication.
The Impact
This was not just improper access control — this was platform-wide privilege escalation with deep implications:
- PII Exposure: Full name, email, phone number of every employee, customer, and EMI user.
- Store Takeover: Any actor could become admin of any store — potential for insider fraud is massive.
- Compliance Nightmare: A breach of this nature could potentially violate GDPR-like regulations in India.
- Infrastructure Abuse: View and control POS machines, terminals, transactions.
In the wrong hands, this would be full database compromise.
Lessons Learned
- Never trust the client: especially not for roles and access rights.
- GraphQL doesn’t mean secure: enforce access at every layer, not just by field names.
- TID-based flows are weak: use multi-factor validation with tamper-proof tokens.
Ending thoughts
Bugs like this remind me that the simplest assumptions often cause the biggest breaches. Always question default flows. Because somewhere out there, someone’s just one mutation away from becoming the boss.
Stay curious. Stay safe.
