Unauthenticated IDOR in a contract-confirmation flow (PII-hash disclosure)
TL;DR — An endpoint that needed no auth, ignored the user ID, and keyed on a sequential contract ID — leaking an internal GUID and md5(email) for any contract. A redacted writeup of an accepted finding.
An accepted finding from a private program — the blackbox bug bounty of a European energy provider. The program name, hosts and all customer data are redacted; the technique is what matters.
TL;DR
A contract-confirmation API endpoint — the step a customer hits right after registering — was:
- reachable without authentication (every other user endpoint returned
401; this one didn't), - missing object-level authorization (the user-ID in the path was ignored), and
- keyed on a guessable, sequential integer (the contract ID).
Chained, those three let an unauthenticated attacker iterate contract IDs 1..N and harvest, for every contract, an internal registration GUID and md5(customer_email).
Class: IDOR / BOLA (CWE-639). Severity: Medium (CVSS 5.3). Status: accepted.
The endpoint
GET /api/v3/Contracts/confirm/{contractId}/{userId}/{device}
Three things stood out:
- No auth required. User-scoped endpoints returned
401without a Bearer token — this one returned302/400/500. Its OpenAPI definition even declaredsecurity: null. {userId}is ignored. The correct GUID, an all-zero GUID, or a random one all returned an identical response. The object was selected by{contractId}alone.{contractId}is sequential. My freshly-created test contract was a small integer, so every contract is directly addressable by counting up.
Proving it — on my own data only
The golden rule in a live program: never pull another user's data. So I proved the missing authorization against my own contract, with the all-zero-GUID trick:
# 1. Baseline — a normal user endpoint needs a Bearer token
curl -s -o /dev/null -w '%{http_code}\n' \
'https://api-dev.<redacted>/api/User/userprofile/personal?api-version=1.0'
# => 401
# 2. Call confirm UNAUTHENTICATED with my contractId + a valid-looking userId GUID
curl -s -D - \
'https://api-dev.<redacted>/api/v3/Contracts/confirm/<myId>/<guid>/0?api-version=3.0'
HTTP/2 302
location: https://<redacted>/contract-confirmed?id=<myId>
®istration_id=<server-side GUID I never supplied>
&bypassId=true
&emailHash=<md5 of my own account email>
# 3. Same request, userId replaced with an ALL-ZERO GUID -> identical response
# same registration_id, same emailHash
# => userId is ignored; the object is keyed on contractId alone
The 302 leaked server-side values I never sent: an internal registration_id and emailHash = md5(lowercase_email). Confirming the PII link was trivial — md5() of my own account email matched the leaked hash exactly.
I did not enumerate anyone else's contracts; the zero-GUID test on my own object already proves the broken authorization.
Impact
- Unauthenticated bulk disclosure. Iterate
contractId→ build a{registration_id, md5(email)}map of the entire customer base.md5(email)lets an attacker confirm whether a known address is a customer and tie contract IDs to specific people;registration_idis an internal key reusable in other flows. - A KYC-bypass flow was exposed. The redirect carried
bypassId=true— this path skips ID/KYC verification. Combined with the absent auth, a sensitive onboarding action was addressable for arbitrary contracts (described from observed behaviour, not exploited). - On a regulated onboarding flow, that's privacy + compliance risk, not just an info leak.
Remediation
- Require authentication — or an unguessable, single-use, contract-bound token — on the confirmation endpoint, and reject any request where the caller doesn't own the contract.
- Stop emitting
registration_id,emailHashandbypassIdin a client-side redirect URL. - Replace
md5(email)identifiers with opaque random tokens, and use non-sequential (GUID) contract IDs so objects can't be guessed by incrementing an integer.
Lesson
Three "small" issues — no auth, an ignored user ID, a sequential ID — are each a shrug on their own. Chained, they're an unauthenticated PII-harvesting primitive. Always test the onboarding steps (confirm / activate / verify), and always check whether the server hands back values you never sent.
Path traversal to a constrained SSRF: reading an internal Drupal host
A public content-listing endpoint accepted ../ sequences in a path parameter and relayed them to an internal Drupal backend, returning internal JSON the Internet can't reach. A redacted SSRF writeup.
Hijacking a trusted app's WebView via an exported Android Activity
An energy app shipped a device-linking SDK whose Activity was exported with no permission, parsed an unsigned base64 token, and loaded attacker URLs into a JS-enabled WebView wired to native Bluetooth + OAuth bridges. A redacted writeup of an accepted finding.