An independent security audit for the app you built on Supabase
Supabase ships a real authorisation system, and the defaults are reasonable. What goes wrong is shipping before anyone goes back to them, because while you are the only account in the database every screen looks correctly scoped. This audit establishes which of those decisions are still open in your application.
What each layer can establish here
Three layers, each answering a different question about the same application. You can stop at any of them, and the report says which ones ran and what they could not reach.
Repository validation
E2Reads the source the project is built from: the policies your migrations declare, which key is reachable from client code, whether authorisation runs on a server or only in the interface, and which dependencies carry known critical vulnerabilities. Findings cite the file they came from. It sees what the repository declares, so a table created by hand in the dashboard is outside its reach.
Runtime validation
E4Builds and runs the application in an isolated environment and probes it. This is the layer that answers what reading a policy cannot: whether the deployed system actually refuses a read of data belonging to another account. A policy that looks right and a policy that held under a request are different claims, and only one of them is evidence.
Public URL scan
E4Tests the deployed application from outside: transport security, security response headers, and what the running service discloses about itself. It cannot see a policy, and it does not report the publishable key in your bundle, because that key is meant to be there. What it does observe is observed against the running system; what it cannot reach stays uncovered rather than assumed.
Where a Supabase application usually gives way
These are the failure modes common in this architecture, not claims about your application. The audit establishes which of them are actually present in yours.
Row level security that is on and scopes nothing
Enabled is not the same as protected. A table can have row level security switched on and still return every row, because the policy behind it is a permanent true, or it asks only whether the caller is signed in. If sign-up is open, signed in is a public category. The mistake is rarely the missing switch, it is the policy that scopes nothing.
Two keys, and only one of them is a secret
The publishable key, historically the anon key, is designed to ship in a browser. Finding it in a bundle is not a vulnerability, and it is safe there precisely because row level security decides what it can reach. The service role key is the opposite: it bypasses your policies entirely and must never leave a server. Treating both as one problem is how the real exposure gets lost in the noise.
Being signed in is not permission to act
Authentication arrives working, which makes it easy to read a valid session as an answer to a question it never asked. Authentication establishes who is calling. Authorisation decides whether that caller may read or change this particular row, and it has to be written down somewhere: in a policy, or on a server you control.
Storage buckets nobody gave rules to
Objects in storage are governed separately from your tables. A public bucket serves every object to anyone with the URL, and a private bucket with no object policies is a feature that does not work until someone loosens it under time pressure. Upload paths are the usual case, and the audit checks whether the rules on a bucket match what the application assumes about it.
Database functions that run as their author
A function declared security definer executes with its creator's privileges, so the policies that constrain the caller do not apply inside it. That is sometimes exactly the intent, which is why the option exists. It also makes every such function an authorisation boundary of its own, responsible for checking what its caller may do rather than assuming the database already did. Leaving its search path unpinned widens the same problem, and any RPC reachable from the client deserves the same reading.
Cross-tenant isolation nobody tested as isolation
A multi-tenant product carries a tenant column and a set of rules that are supposed to keep one customer out of another customer's rows. Whether they do is a question about behaviour, not intent, and reading the schema does not answer it. Asking the running system for someone else's data does.
What the result looks like
An illustrative extract, not a real customer result. Every row carries the evidence grade behind it, and a control nobody could establish says so.
| Control | Status | Evidence | Note |
|---|---|---|---|
| Row level security enabled on tables holding user data | Failed | E2 | Two tables in the migrations are exposed with no policy |
| Privileged keys absent from client code | Verified | E2 | Only the publishable key is reachable from the bundle |
| Cross-account read refused by the running system | Verified | E4 | A request for another account's rows returned nothing |
| Server-side authorisation on state-changing endpoints | Partial | E2 | Ownership checked on writes, absent on two updates |
| Storage bucket rules | Uncovered | Not graded | No evidence available from the layers that ran |
Mapped to published standards
Each control is tested against a named clause, so a result means something outside our own vocabulary. Mapping is not certification, and none of these bodies endorse StackAttest.
What this audit is not
- It does not connect to your Supabase project. There is no dashboard integration and nothing asks for your project keys. StackAttest validates the application that was built and, where you run that layer, the deployed system it becomes.
- It is not a penetration test. A deterministic check and a runtime probe are not an adversarial human engagement, and there are classes of weakness only a person hunting for them will find.
- It is not SOC 2 and does not replace it. SOC 2 attests to organisational controls over time; this validates technical controls in the software. They answer different questions for different buyers.
- It does not replace human due diligence. It gives a reviewer repeatable evidence to start from, which is a different thing from being the reviewer.
- Controls the run could not reach are reported as uncovered rather than passed. A result you did not earn is worse than no result.
Questions
What does a Supabase security audit cover?
Row level security and the policies behind it, how the two keys are handled, authentication against authorisation, storage rules, database functions and RPC, and whether one account can reach another account's data. Each result carries the strength of the evidence behind it, and anything the run could not reach is listed as uncovered.
My Supabase key is visible in the browser. Is that the finding?
Almost certainly not. The publishable key, historically the anon key, is designed to sit in client code, and it is safe there because row level security decides what it can reach. The key that matters is the service role key, which bypasses policies entirely. An audit separates the two rather than reporting every key it finds.
Do you connect to my Supabase project?
No. There is no integration and no dashboard access. Source evidence comes from your repository through a read-only connection, and runtime evidence comes from building and probing the application. Anything created directly in the dashboard and absent from the repository is outside what those layers see, and the report says so rather than assuming.
Can you tell me whether my policies actually work?
Reading a policy tells you what it says. Runtime validation attempts a read of one account's data while authenticated as another and records what the deployed system returned, which is the difference between a policy that looks right and a policy that held. Where a run cannot reach that behaviour, the control is reported as uncovered.
Is Supabase insecure?
No, and we would not say so. It ships a real authorisation system and its defaults are reasonable. The failures we see belong to applications that were assembled quickly and shipped before anyone went back to the rules, which is a property of the build rather than of the platform.
What about security definer functions and RPC?
They are part of the audit. A function that runs with its creator's privileges is not constrained by the caller's policies, so it has to do its own checking. Any such function reachable from the client is read as an authorisation boundary in its own right rather than as ordinary application code.
Related
Find out what is actually true of your application
Start with the layer you can run today. The report says which layers ran and what they could not reach, so the result is honest about its own limits.