← All writeups
High Bugcrowd · PlanetHoster Duplicate

Enumerating an Invoice Database Without Logging In: Unauthenticated IDOR at PlanetHoster

idoraccess-controlunauthenticatedfinancial-dataresponsible-disclosure

By Kavennesh Balachandar · Bugcrowd · PlanetHoster Bug Bounty Program

# TL;DR

PlanetHoster's "pay invoice as a guest" feature is backed by an unauthenticated REST endpoint that keys its lookup solely on a short sequential integer invoice ID. It answers HTTP 200 with no cookie and no Authorization header, and returns a clean structured not-found (no auth challenge) for bad IDs — so the numeric ID is the only thing gating the request. Because IDs are low-entropy and sequential, an unauthenticated attacker can walk the range and read arbitrary customers' invoice records. Assessed High / P2 (CVSS 7.5) — contingent on the data-exposure confirmation, which the app's own client code indicates. As much as the bug, this write-up is about how to report a sensitive-data IDOR without ever touching a real customer's data.

# The endpoint

GET https://my.planethoster.com/v2/api/myspace/invoice/info-as-guest/{id}/view
Auth:   none (no cookie, no Authorization header)
Backend: Ruby on Rails (LiteSpeed)

It wasn't guessed. The Angular client at /v2/pay-invoice calls mys.viewInvoiceAsGuest(id), which issues this.get("/myspace/invoice/info-as-guest/${id}/view"), and the base API service prefixes the configured baseUri: "/v2/api". The {id} segment is the sequential numeric invoice ID.

# What was directly observed

The endpoint answers unauthenticated, with no cookies or token:

GET /v2/api/myspace/invoice/info-as-guest/999999999/view HTTP/2
Host: my.planethoster.com
Accept: application/json
HTTP/2 200
content-type: application/json; charset=utf-8
x-runtime: 3.398512
cache-control: no-store
(no Set-Cookie, no WWW-Authenticate, no auth redirect)

{"success":false,"errors":[],"data":{}}

999999999 is a deliberately out-of-range sentinel, chosen so no real customer data is touched. The 200 with a clean "not found" and no auth challenge and no "missing token" error proves the endpoint is fully anonymous and resolves invoices purely by the numeric ID (the ~3.4s x-runtime shows a real server-side lookup happened).

The IDs are sequential and enumerable. Real, in-range invoice IDs recovered passively from the public Wayback Machine archive of /pay-invoice?id=N fall in a ~890,000–1,200,000 range — 6–7 digit incrementing integers. An attacker can walk the space with simple incrementing requests, one per invoice.

# What a valid response exposes (from client code)

The Angular client parses and renders these fields from this endpoint's response:

invoice_id, amount, balance, currency_code, status,
client_secret,                       // Stripe payment-session secret
gateway_info { paypal { receiver_emails }, moneybookers, stripe }

That's the invoice owner's amount owed / balance, payment status, and payment-gateway metadata — and if client_secret is a live Stripe PaymentIntent secret, per-invoice payment-session secrets too.

# The responsible-disclosure decision

Here's the part worth dwelling on. To prove the data exposure, the obvious move is to request a real, valid invoice ID and show the populated response. I didn't do that — because that response would be a third party's financial record, and accessing it to prove a point is exactly the harm the report is meant to prevent. I also couldn't self-provision an invoice for a clean self-owned proof: account/payment creation is blocked by the provider's MaxMind fraud screening.

So the report separates observed from indicated:

  • Observed: the endpoint is unauthenticated, keys on the numeric ID, returns a clean not-found for bad IDs, and IDs are sequential.
  • Indicated (not retrieved): that a valid ID returns the owner's financial data — established by the application's own client code, which renders those fields from this exact endpoint.

And it hands the final step to the team: please confirm internally by issuing the request with any one valid invoice ID — a one-step check that keeps customer data out of a researcher's hands entirely. That's the right way to land a sensitive-data IDOR: prove the mechanism conclusively, and let the data owner confirm the payload.

# Impact and severity

An unauthenticated, internet-based attacker can enumerate the sequential invoice range and read arbitrary customers' billing records — amount, balance, payment status, gateway metadata — one GET per invoice, no account or victim interaction. Bulk exposure of customer financial data across the customer base.

Severity: High — CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N (7.5) → P2, contingent on the Step-3 data confirmation the client code indicates. If the returned fields were shown to be non-identifying, the floor is P3; the presence of financial amount + gateway metadata keeps the assessed rating at P2. (The program triaged the report lower / as a duplicate; the assessed rating is the defensible ceiling given what the client code exposes.)

# Remediation

  • Authenticate or token-gate the lookup — require an authorized session, or bind guest access to an unguessable per-invoice token (UUID/HMAC) delivered out-of-band in the invoice email, never the sequential integer.
  • Enforce object-level authorization — a request may only resolve an invoice the caller owns or holds a valid token for.
  • Rate-limit / add anomaly detection to blunt enumeration.

# Disclosure timeline

  • 2026-06-24 — Reported to PlanetHoster via Bugcrowd. All testing tagged with a bug-bounty research header; only the out-of-range sentinel 999999999 was ever queried against the endpoint.
  • Closed as duplicate / informational.
  • Public disclosure pending program approval.

# References