Unauthenticated Supabase RPC Exposes Cross-Tenant Invoice Data
The Risk
Anyone on the internet could retrieve private billing records from a healthcare invoicing system without logging in. A single web request returned invoice details for multiple organizations, including business names, locations, amounts owed, and direct links to view or download payment documents. No password, no account, and no special tools were needed.
The Vulnerability
1. Public configuration exposed backend credentials
The web application loaded its configuration from a publicly accessible JavaScript file. This file contained the Supabase project URL and the anonymous API key used by the client application. While the anonymous key is expected to be public in Supabase deployments, it becomes dangerous when backend functions do not enforce their own access controls.
2. RPC function executable without authentication
A Supabase PostgREST RPC function was configured to be callable by the anon (unauthenticated) role. The function was designed to retrieve invoice records for email-based actions, but it performed no authentication or authorization checks before returning data. Calling it with an empty parameter set returned a bulk dump of 50 invoice records across multiple tenants.
POST /rest/v1/rpc/get_invoices_for_email_action
Content-Type: application/json
apikey: [public_anon_key]
{} No session cookie, no Authorization header, and no bearer token were required. The anonymous API key alone was sufficient to execute the function.
3. Verbose error hints aided discovery
GraphQL introspection was enabled on the Supabase project, allowing full schema enumeration including all RPC function names. Additionally, PostgREST returned verbose error hints when an incorrect function name was used, suggesting the correct function name in the response. While these features assisted discovery, they are not required for exploitation once the endpoint is known.
The Attack
- The attacker visits the public web application and extracts the Supabase project URL and anonymous API key from the client-side configuration file
- Optionally, the attacker uses GraphQL introspection to enumerate all available RPC functions, or tests function names and reads the correction hints from error responses
- The attacker sends a POST request to the vulnerable RPC endpoint with an empty JSON body and only the public API key as authentication
- The endpoint returns 50 invoice records containing billing data, organization identifiers, and direct links to payment processor documents
- The attacker follows the returned payment processor URLs to view full invoice details and download PDF copies
The Impact
Each invoice record returned by the unauthenticated endpoint contained:
| Data Category | Fields Exposed |
|---|---|
| Organization identity | Business name, location (state), group affiliation |
| Billing details | Invoice number, amount due, amount paid, currency, payment status |
| Temporal data | Creation date, due date, finalization date, days overdue |
| Payment processor links | Hosted invoice URL (viewable), invoice PDF (downloadable) |
| Internal identifiers | Customer IDs, payment processor IDs, internal group IDs |
Verified payment processor access
The hosted invoice URLs and PDF download links returned by the API were confirmed to be accessible. Opening the hosted invoice URL loaded a full invoice view on the payment processor's platform. The PDF link triggered a direct download of the invoice document. Both were reachable without additional authentication.
Cross-tenant exposure
The 50 records returned in a single request contained invoices belonging to multiple organizations across different geographic regions. This was not a single-tenant data leak but a cross-tenant exposure of the entire invoicing dataset accessible to the function.
Remediation
- Revoke unauthenticated execution on the RPC function by removing the
EXECUTEgrant from theanonrole - Add authentication enforcement inside the function body so that requests without a valid session are rejected with a 401 or 403 response
- Implement tenant scoping so that authenticated users can only retrieve invoices belonging to their own organization
- Audit all RPC functions for execution grants to the anonymous role, sensitive return values, and missing authorization checks
- Reduce PostgREST error hint verbosity to prevent function name leakage
- Restrict GraphQL introspection in production environments