Enumerating an Invoice Database Without Logging In: Unauthenticated IDOR at PlanetHoster
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
999999999was ever queried against the endpoint. - Closed as duplicate / informational.
- Public disclosure pending program approval.