y4ss3r
cyber-sec engineer
← cd ../writeups
Bug Bounty/API · Web/Intermediate/Oct 2026/7 min

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.

Bug BountyIDORAPIAccess Control
[ machine card / cover ]

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:

  1. No auth required. User-scoped endpoints returned 401 without a Bearer token — this one returned 302/400/500. Its OpenAPI definition even declared security: null.
  2. {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.
  3. {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>
    &registration_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_id is 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

  1. 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.
  2. Stop emitting registration_id, emailHash and bypassId in a client-side redirect URL.
  3. 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.

YL
Yasser LaroussiCPTS · eJPTv2
Next writeup
... /Related ...
Bug Bounty/Web · SSRF/Medium/6 min

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.

WebBug BountySSRFDrupal
Read moreOct 2026
Bug Bounty/Android · Mobile/Hard/8 min

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.

MobileAndroidBug BountyWebView
Read moreOct 2026