Disclosure · Broken access control

Thousands of users exposed and a wide-open storage bucket

Leer en español

This client was a platform with registered users and third-party listings. The interesting part wasn't a single finding but the pattern that repeated over and over: authentication was confused with authorization, and identity checks were done on the client. The result: personal data of thousands of people reachable without credentials, and a storage bucket anyone could write to.

1. Full seller PII via a URL parameter

The endpoint that returned a listing's detail had two views controlled by a flag: ?admin=true returned the "rich" version. That flag checked nothing.

# No credentials, with the admin flag:
curl "https://api.<backend>/api/db/<endpoint>?id=<uid>&admin=true"
# -> { "name":"[redacted]", "email":"[redacted]", "phone":"[redacted]",
#      "<campo>": { "titular": { "cuit":"[redacted]", "nro_documento":"[redacted]" },
#                               "domicilio_titular": { "calle":"[redacted]", "numero":"[redacted]" } } }

Enumerating the public listing UIDs and repeating the request yielded name, email, phone, tax ID, national ID and home address of every active seller, with no credentials at all.

2. The registered-user list with a free sign-up

The /api/users/<endpoint> endpoint existed for the admin panel. It required a token but did not check the role. Since sign-up was open, anyone registered in seconds and got the user list:

# 1) Free account -> valid token
curl -X POST "https://identitytoolkit.googleapis.com/v1/accounts:signUp?key=<public_api_key>" \
  -d '{"email":"anyone@test.com","password":"Pass1234!","returnSecureToken":true}'

# 2) With that token, list registered users (email + name + uid)
curl -H "Authorization: Bearer $TOKEN" "https://api.<backend>/api/users/<endpoint>"
# -> 1,000 users

The endpoint returned 1,000 at a time and accepted no pagination, but the limit wasn't the impact: any user could read the whole registry.

3. Buyer emails hidden in internal fields

The public listings endpoint returned, per ad, an array with the emails of everyone who clicked "contact via WhatsApp". The per-item detail returned another with those who favorited a listing. Neither field should have been public:

SourceData exposed without auth
<endpoint> → <campo>[]email + name of each prospect
<endpoint> → <campo>[]plaintext emails of users who favorited

The juicy detail: <campo> also showed up in the bulk listing, while <campo> only in the single item. An analysis that only looks at the listing misses them.

4. The unlocked Storage bucket

The cloud storage serving all photos (listings and profiles) had no effective security rules. With no auth header at all:

  • Read any object without the download token the provider normally requires.
  • Write (POST) and overwrite any object.
  • Delete (DELETE) any object.
# Write an arbitrary object, no credentials
curl -X POST "https://firebasestorage.googleapis.com/v0/b/<project>/o?name=pentest/poc.txt" \
  -H "Content-Type: text/plain" --data "test"
# -> 200, object created

# Delete the same object, no credentials
curl -X DELETE "https://firebasestorage.googleapis.com/v0/b/<project>/o/pentest%2Fpoc.txt"
# -> 204

Uploading with Content-Type: text/html was served inline, executing in the browser under the provider's domain. In practice: deleting any listing's photo, or hosting phishing and XSS under a Google URL that looks legitimate.

Why it happened

Two root causes, the same one underneath. First, using Origin as authentication: the API rejected requests without the Origin header and accepted Origin: https://<domain>, which anyone can forge with curl. Second, authorization decided on the client: the admin panel checked access by comparing emails in JavaScript, and several routes never re-checked the role server-side. A client-side check is not a control.

Impact

  • Seller PII with identity data (tax ID, national ID, address): a base for fraud and targeted phishing.
  • Purchase-intent profiling: which person was looking at which listing, at what price.
  • Content destruction: deleting or defacing every photo on the platform.
  • Legal exposure: third-party personal data in Argentina, covered by Personal Data Protection Act 25.326.

Remediation

  1. Drop the admin=true logic and split it into an endpoint that requires a server-verified role.
  2. Verify the role (not just the session) on <endpoint> and every admin route.
  3. Remove <campo> and <campo> from public responses; replace them with counts.
  4. Write Storage rules requiring auth for writes and deletes, and restrict Content-Type.
  5. Replace Origin with real token validation and enable the provider's App Check.
← Back to Security