← Writeups

IDOR on CoinDepo: reading any user's KYC through a trusted email field

One profile endpoint decided whose data to return from values in the request, not from who was logged in. Any authenticated user could pull another user's full KYC record. Reported through HackenProof, rated Critical, rewarded $5,289.

CoinDepo is a crypto savings and lending platform, so the accounts on it carry real identity data. One of its API endpoints returned a user's full profile and KYC record (name, date of birth, phone, address, and links to their uploaded ID documents) based on identifiers sent in the request, without checking that the person asking actually owned that account. Any logged-in user could read any other user's record by changing a single field.

It was reported through HackenProof, disclosed, rated Critical, and rewarded $5,289.

The HackenProof report CODWADDA-7 on the CoinDepo program: state Disclosed, severity Critical, category Insecure Direct Object Reference, bounty $5,289
The disclosed report on CoinDepo's HackenProof program: Critical, category Insecure Direct Object Reference, bounty $5,289.

What an IDOR is, in plain terms

When you open your profile, your app asks the server: give me my details. The server has to decide whose details to hand back. There is a safe way and an unsafe way to make that decision.

When the server trusts an identifier in the request (an email, a user id) and returns that person's data without checking it belongs to you, that is an Insecure Direct Object Reference (IDOR), also called Broken Object Level Authorization (BOLA). You point directly at someone else's object, and nobody checks whether you are allowed to. It is the most common serious API bug there is, and it is exactly what happened here.

The endpoint and the flaw

The profile page loads through POST /api/details. The request carries several identifier fields in its body (among them email, username and a client_token) and the endpoint returns the profile and KYC record for the account those fields name.

The mistake

The endpoint picked the account from the email and identifiers in the request body, rather than from the authenticated session. Nothing on the server checked that the caller owned the account they were asking about. So changing the email to someone else's returned someone else's data.

Reproducing it

Testing was done with two accounts I created and controlled, so no real user's data was ever touched. The steps:

  1. Log in with any valid account

    Any authenticated account is enough. There is no special role required.

  2. Catch the profile request and change the target

    In the email-verification flow the browser sends a request to /api/details. Send it to a proxy like Burp Suite, then change the identifier fields to point at another account.

    POST /api/details HTTP/2
    Host: app.coindepo.com
    Content-Type: multipart/form-data; boundary=----boundary
    ...
    
    ------boundary
    Content-Disposition: form-data; name="username"
    
    VICTIM_EMAIL@example.com          <-- the account you want to read
    ------boundary
    Content-Disposition: form-data; name="email"
    
    VICTIM_EMAIL@example.com          <-- same here
    ------boundary
    Content-Disposition: form-data; name="client_token"
    
    <a valid token from any account>
    ------boundary
    Content-Disposition: form-data; name="cdkey"
    
    <request key>
    ------boundary--
    
  3. Send it, and read the response

    The endpoint answers 200 OK with the target account's full record.

Burp Suite showing the request to /api/details on the left with a swapped email, and the response on the right containing the account's name, date of birth, phone, address and ID document paths. The client token and request key are redacted.
Left: the request, with the target's email in the body. Right: the record that came back: name, date of birth, phone, address, and paths to the uploaded ID documents. Both accounts were mine, so the data shown is my own test data.
What is KYC?

Know Your Customer: the identity data a financial platform collects to prove you are who you say: your ID card, a selfie, your address. It is some of the most sensitive data a company ever holds about a person.

What came back

The response was the account's complete profile. The fields that matter, sanitized:

{
  "user_id": 19090,
  "email": "VICTIM_EMAIL@example.com",
  "first_name": "...",
  "last_name": "...",
  "date_of_birth": "YYYY-MM-DD",
  "phone": "+...",
  "address1": "...",
  "city": "...",
  "state": "...",
  "zip": "...",
  "document_type": "id-card",
  "document_photo1": "./uploads/<...>_document_photo1.jpg",
  "document_photo2": "./uploads/<...>_document_photo2.jpg"
}

The last two lines are the worst of it: paths to the images of the account holder's government ID. Full name, date of birth, address and a phone number is already enough to impersonate someone; add their ID documents and it becomes a complete identity kit.

Proof of concept

The endpoint returning a different account's full profile after only the identifier fields are changed.

Why this is Critical

On a financial platform this is not just a privacy leak.

The fix

IDOR is an authorization problem, so the fix is about who the server trusts to name the account:

Key takeaways

  • If an endpoint decides whose data to return from a value in the request, assume it can be pointed at someone else until proven otherwise.
  • Authorization must be derived from the session, never from a field the caller controls.
  • The severity of an IDOR is set by what it returns: here, KYC and ID documents, which makes it Critical.
  • Test object-level access on identity endpoints specifically; they are where a leak hurts most.
IDOR BOLA Broken Access Control API PII
Share on X

More writeups