What the page hid, the API handed over

A lot of product teams quietly assume that if a field isn't shown on the page, it isn't exposed. The profile screen renders a username and a photo, the sensitive details are left out of the markup, and everyone moves on believing those details are private.

That assumption skips a layer. Behind the page is an API, and the API often returns far more than the page chooses to display. The front end drops the extra fields on the floor; anyone who talks to the API directly picks them right back up.

On a recent assessment of a large adult live streaming platform, that gap was the whole finding, and the platform is exactly the kind where it matters most. Two API endpoints returned a user's full profile object. The public profile page showed almost none of it. The API returned date of birth, real name, city, ethnicity, sexual orientation, stated preferences, a personal biography, and the account creation date. None of those appeared anywhere in the page source. They came back only through the API.

Severity: CVSS 7.5 (High) · CWE-639, CWE-213, CWE-306
Exposed: date of birth, real name, city, ethnicity and sexual orientation across a user base running into tens of millions of sequential IDs, with no authentication and no rate limit.

Why this is worse than a normal data leak

Two design choices turned a quiet over sharing bug into mass exposure.

First, the user identifiers were sequential integers, and the identifier space ran into the tens of millions. Sequential means guessable. You do not have to find users, you count. Start at one, increment, repeat.

Second, the endpoints required nothing. No login, no session cookie, no API key, no CAPTCHA, and no rate limiting. A plain unauthenticated GET in a loop walks the entire range at whatever speed the network allows.

Put those together and the profile API stops being a lookup for one user and becomes a bulk export of the user base. One leaked record would be a finding; here every record was reachable, one number at a time, by anyone.

The detail that makes it not just public data

The obvious pushback on any profile finding is "that information is public anyway." The way you kill that argument is a direct comparison, and it is worth doing on every finding of this shape.

Take a single account. Load its public profile page and search the raw HTML for the sensitive values. Date of birth, not there. City, not there. Orientation, not there. Then call the API for the same account and read them straight out of the JSON. Same user, same moment, two very different answers. The web interface intentionally hides these fields. The API serves them anyway.

# the API returns fields the profile page never renders:
$ curl -s https://api.<target>/v2/users/10472*** | jq '{birthDate,city,interestedIn,ethnicity}'
{ "birthDate":"1984-**-**", "city":"Manchester, UK",
  "interestedIn":"Men", "ethnicity":"White" }

# same account, public page -> none of those fields are in the HTML:
$ curl -s https://<target>/u/q****guy_84 | grep -ic '1984\|Manchester\|interestedIn'
0

# IDs are sequential and the endpoint needs no auth -> walk the whole base:
$ for id in $(seq 1 40000000); do curl -s https://api.<target>/v2/users/$id; done

That side by side is the proof that this is excessive data exposure, not scraping of something already on display. The platform made a decision to keep these fields off the page. The API never got the memo.

One response, reconstructed

Here is the shape of a single profile the endpoint returned, with the identifying values masked. This is a synthetic illustration rather than a real person's record, but every field shown is one the API actually returned and the public page did not.

username        q****guy_84
name            J*** C***
birthDate       1984-**-**
city            Manchester, United Kingdom
ethnicity       White
interestedIn    Men                 (sexual orientation)
specifics       [ ... ]             (stated sexual preferences)
description     "hi it's J***, manchester based, add my telegram @j***_real
                 or snap j***xo, customs welcome, verification on request"
                                     (free-text bio, where users routinely typed
                                      a real first name, city, and contact handles)
amazonWishlist  amazon.com/.../wishlist/...
createdAt       2019-05-16

Read it the way an attacker does. The username is a throwaway, but the date of birth and city pin down a real human. The orientation and ethnicity are protected characteristics and the exact attributes used for targeted harassment. The wishlist links to a shopping profile that usually carries a shipping name. The biography is whatever the person typed when they were sure no one was indexing it. One row, and someone who believed they were anonymous no longer is. Now repeat that by counting from one.

Severity lives in the data, not in the bug class

The vulnerability class here is unglamorous. It is broken object property level authorization, the API returning fields the caller has no right to, combined with unauthenticated enumeration. On a generic business app you might call it a medium and move on.

The severity of an information exposure tracks who the people are and which fields leak, not the bug class on the report. That's the thing to carry out of this one.

Consider what was exposed and against whom. Membership on this kind of platform is itself sensitive, the sort of fact people keep private from employers, families, and governments. Now layer the fields on top. Exact date of birth and city are the two values that, combined, deanonymize a person and unlock other accounts through knowledge based recovery. Ethnicity and sexual orientation are protected characteristics in most of the world and are precisely the attributes used for targeted harassment, extortion, and worse. Some of the biographies even carried real world contact details that users had typed in themselves.

The same technical flaw on a recipe website is a shrug. Here it is a deanonymized, attribute tagged list of a population that is uniquely vulnerable to coercion. The bug did not change. The blast radius is set entirely by the data behind it.

The fix

  1. Return only the fields the caller is authorized to see. The API, not the front end, is the authorization boundary. Hiding a field in the UI while the API still serves it controls nothing; it just hides the leak from people who use the page as intended.
  2. Authenticate access to personal data, and scope it. An unauthenticated request should get the bare public minimum. Sensitive attributes should require a session and a legitimate relationship to the record.
  3. Make enumeration expensive. Rate limit per source, add anomaly detection for sequential access patterns, and prefer non sequential identifiers so the ID space cannot simply be counted.
  4. Minimize at the source. Ask whether exact date of birth, city, ethnicity, and orientation need to exist in a profile object at all. The safest field is the one you do not return, and the one you never collected.

Prevent it in CI/CD: field-level authorization tests at the API (assert an unauthenticated call returns only the public minimum); rate-limiting and anomaly detection on sequential access; non-sequential identifiers; and a data-minimization review that questions whether sensitive attributes need to be returned — or stored — at all.

Run this against your own APIs

Teams spend enormous effort deciding what to show on the page. Far fewer ask what the API hands back to anyone who skips the page entirely. Put the authorization check where the data is served, not where it's rendered, because that's the door an attacker actually walks through.