← Back to all reports

Investor Account IDOR Leaks Any User's Identity Record on a Wealth Platform

Reported May 8, 2026
Severity Critical
Platform Web
Vulnerability Class IDOR / Broken Access Control (CWE-639)
Target Type Investment / Wealth Management
Impact Cross-account read of any user's stored identity record

The Risk

Any logged-in investor on the platform could read the stored identity record of any other customer, with a single normal request. The leaked record included full legal name, address, phone number, and a direct pointer into the identity-verification provider that holds the customer's Social Security number, date of birth, and document images. Customer IDs were short numbers, so an attacker could walk the entire customer base in a few hours. The same request also overwrote the victim's name to a broken value that then appeared on the victim's own dashboard until staff manually restored it.

The Vulnerability

The platform exposed a customer-facing investor account API at /a/api/investor-entity/{id}. Every neighbouring sub-path on that resource correctly enforced an ownership check and returned 403 when called cross-account. One sub-path, PATCH /a/api/investor-entity/{id}/provider/etc, did not. The handler accepted a body, fetched the target entity from the database by ID, performed a third-party identity-provider call, persisted the response, and returned the stored entity, all with no request.user.id == entity.userId check.

Two structural facts made the break full-database in scope. First, entity IDs were short sequential integers exposed in normal URLs. Second, the response payload contained every stored field on the target entity record, not just the fields the request body touched. A one-field request body returned a multi-field response with the victim's data.

The Attack

Step 1: baseline read

From a victim session controlled by the researcher, read the victim entity normally and note the stored display name.

curl -s -b "Auth-Token=$VIC" \
  "https://<platform>/a/api/investor-entity/$VICTIM_ENTITY" | jq .investorEntity.name

Step 2: cross-account PATCH from the attacker session

The minimum body that passed schema validation was a single field. From the attacker session (a separate researcher-owned account), call the vulnerable sub-path against the victim entity ID:

curl -s -X PATCH -b "Auth-Token=$ATK" -H 'Content-Type: application/json' \
  "https://<platform>/a/api/investor-entity/$VICTIM_ENTITY/provider/etc" \
  -d '{"owner":{"ssn":"000000000"}}'

Response: 200 with the victim's full stored entity record. Fields present in the response that were never in the request body included the victim's user ID, stored first and last name, account creation timestamp, linked third-party identity provider and banking provider names, stored legal address, phone number in international format, the identity-verification provider's user record ID, and the IDs of every linked investor account.

Step 3: confirm the integrity tamper

Re-reading the victim's entity from the victim session showed the stored display name now set to a literal broken value, persisted across sessions on both the entity and every linked investor account. Refreshing the victim's own dashboard, the greeting still used the intact first-name field but the account widget rendered the corrupted name string. Recovery required staff to restore the field from the identity provider source.

Step 4: authorisation comparison

Three neighbouring routes on the same entity path returned 403 from the attacker session, while only the vulnerable sub-path returned 200:

403  POST  bank-account/default
403  POST  investor-account
403  PATCH user/investor-profile
200  PATCH provider/etc

The auth check existed throughout the rest of the resource. It was missing only on this one sub-path.

Real-user evidence

A single real production entity was probed during initial discovery and re-confirmed on a separate day. The response leaked the user's full legal address, phone number, identity-provider user record ID, and confirmed the third-party identity-verification provider and banking provider names. The corrupted name field persisted across both reads, demonstrating that the integrity tamper survived in production until manually remediated.

The Impact

For each cross-account hit, the response leaked: full legal first and last name, full legal address (street, city, state, ZIP), phone number, the identity-verification provider's user record ID (which is a direct pointer into the vault holding the customer's Social Security number, date of birth, and ID-document images), the identity-verification and banking provider names, account creation date, account type, and linked investor-account IDs.

Because entity IDs were short sequential integers, an attacker could enumerate the entire customer base in a few hours of automated requests. Each hit also persisted a broken display name on the victim's record, corrupting a regulated identity field that rendered immediately on the victim's own dashboard, statements, and account widgets. The bug shape and missing check made every user on the platform reachable through identical requests; this is a full-database identity-record disclosure with an integrity-tamper side effect, not a single-user finding.

The finding mapped directly to the program's stated focus areas: cross-account portfolio metadata access and reading data out of the underlying user database.

Remediation

  • Add a server-side ownership check on PATCH /a/api/investor-entity/{id}/provider/etc before any entity lookup, body validation, or response serialisation. Reject with the same 403 investorEntity.forbidden response already used by every neighbouring route when request.user.id != entity.userId.
  • Audit every other handler on the same resource for the same gap. The existing pattern of per-route ownership checks is fragile; consider centralising the check in middleware that runs unconditionally for any path prefixed with /a/api/investor-entity/{id}.
  • Restore the corrupted name fields on every affected entity by re-reading from the identity-verification provider source. Run a one-time scan for any other entities currently stored with the broken display value and restore those as existing victims of this bug.
  • Audit access logs for historical cross-account hits on the vulnerable sub-path where the requester is not the entity owner. Given the missing check has been live, this surface may have been touched before.
  • Replace short sequential integer entity IDs with unguessable identifiers (UUIDs or opaque slugs) so a future missing check does not become enumerable-by-default.