Disclosure · Backend as a Service

Over 280,000 contacts within anyone's reach

Leer en español

This was not a client: it was an online betting platform I reviewed out of public interest, given practices I found at best questionable. Everything below was obtained with the public key the site itself hands every visitor — no account, no session, no brute force. Read-only requests only.

The public key isn't the problem: the rules are

The site was a PWA on a Backend as a Service (BaaS). Shipping the public key in the frontend is normal for that architecture: security doesn't rely on hiding the key but on the access rules (RLS) and on which functions are left callable by the anonymous role. That's where the problem was.

1. Looking up anyone by their national ID, no login

Checkout needed to autofill the name from the national ID. For that there was a backend function. The catch: anyone could execute it, and it returned far more than the name.

POST https://<project>.supabase.co/rest/v1/rpc/<rpc>
apikey: <site_public_key>

{"p_dni":"11111111"}
# -> [{
#      "customer_name": "...",
#      "full_email": "...",
#      "full_phone": "...",
#      "province": "...", "locality": "...", "address": "...",
#      "postal_code": "...", "birth_date": "..."
#    }]

The contacts table held over 280,000 records. Enumerating IDs was enough to harvest name, email, phone, address and date of birth for the whole base — ideal material for targeted phishing and identity theft. The same function was exposed in production and in staging.

2. Internal financial metrics with no authentication

Several aggregate functions accepted the public key and returned internal business numbers:

Function (no auth)What it exposed
bank_transfers_total_amounttotal transfer volume (ARS)
get_contacts_audience_countsize of the contact base
get_cost_summaryBusiness costs
calculate_settlement_previewsettlement and commission preview
get_contact_distinct_filtersinternal source/tag taxonomy

The interesting part: their twins were protected (Unauthorized). So this wasn't intended public behavior, but a permission oversight function by function.

3. Internal tables readable in the staging environment

On the staging project, the anonymous role could read internal tables: invoicing records (amounts, status, who loaded them) and coupon data (codes, max uses, allowed variants). The catalogue is arguably public; coupons and invoicing are not.

4. What it did not allow

An honest report also closes doors. I tested whether the anonymous role could create or modify a bank account and set it as primary (which would redirect payments). Result: not exploitable. The RPC is granted to the anonymous role, but it authorizes internally and responds Unauthorized; and the row rules block inserts and updates. I close the finding as not vulnerable, without inflating it.

The lesson

On a BaaS, every function and every table is a door. Having the public key in sight is expected; leaving sensitive functions callable by the anonymous role is not. Review permissions per object: revoke the anonymous role's EXECUTE, require authorization inside the function, and return the minimum data (never the full email or phone).

Remediation

  1. Revoke EXECUTE from the anonymous role for sensitive functions;
  2. authorize inside the function (staff role) instead of trusting the grant;
  3. return only masked, minimal data;
  4. enable RLS on invoicing and coupons;
  5. add attempt throttling and rotate the published keys.
← Back to Security