Disclosure · Backend as a Service
Over 280,000 contacts within anyone's reach
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_amount | total transfer volume (ARS) |
get_contacts_audience_count | size of the contact base |
get_cost_summary | Business costs |
calculate_settlement_preview | settlement and commission preview |
get_contact_distinct_filters | internal 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
- Revoke
EXECUTEfrom the anonymous role for sensitive functions; - authorize inside the function (staff role) instead of trusting the grant;
- return only masked, minimal data;
- enable RLS on invoicing and coupons;
- add attempt throttling and rotate the published keys.