glossary
Ownership-bound RLS Policies
Ownership-bound policies tie rows to a user or tenant; missing that link leaves data open. This page explains it in plain English, then goes deeper into how it works in Supabase/Postgres, what commonly goes wrong, and how to fix it without relying on fragile client-side rules.
What “Ownership-bound RLS Policies” means (plain English)
A good policy checks that the row’s owner matches the current token before returning it. Ownership-bound RLS Policies is a practical security issue for teams using Supabase because exposed tables, storage, or RPC endpoints can return or mutate data beyond intended account boundaries.
How Ownership-bound RLS Policies works in Supabase/Postgres (technical)
Policy expressions should compare columns like user_id or tenant_id to auth.uid() or tenant claims, otherwise Postgres treats every row as allowed.
Attack paths & failure modes for Ownership-bound RLS Policies
- Policy checks login, not ownership: A marketplace app stores all customer data in one
orderstable and only checks thatauth.uid()exists. - Write policy missing WITH CHECK: The team focused on SELECT and added a restrictive policy, but writes still happen directly from the client.
- Policy checks login, not ownership: The policy is effectively “true for any logged-in user”, so attackers can enumerate rows for every customer.
- Write policy missing WITH CHECK: Users could insert or update rows with another
user_idortenant_id, letting them impersonate or escalate privileges. - Policies check “logged in” but don’t bind rows to user/tenant ownership, enabling scraping and cross-tenant reads.
- Read rules exist, but write rules are missing or broader, enabling spoofed inserts/updates on behalf of other users.
- Policies become a pile of OR conditions that are hard to review and easy to accidentally widen.
Why Ownership-bound RLS Policies matters for Supabase security
Weak ownership checks lead to cross-user leaks, which is the root cause of many BOLA-style breaches. If Ownership-bound RLS Policies remains unresolved, attackers can automate enumeration and unauthorized writes at API speed. Treat it as a production reliability risk as well as a data security risk, because incidents spread quickly once clients discover weak access boundaries.
Common Ownership-bound RLS Policies mistakes that lead to leaks
- Checking only that
auth.uid()exists instead of binding ownership. - Leaving write policies without a
WITH CHECKthat mirrors the read rules. - Assuming a user is on the same tenant as the row without validating it explicitly.
- Policy checks login, not ownership: The policy is effectively “true for any logged-in user”, so attackers can enumerate rows for every customer.
- Write policy missing WITH CHECK: Users could insert or update rows with another
user_idortenant_id, letting them impersonate or escalate privileges.
Where to look for Ownership-bound RLS Policies in Supabase
- Policy conditions: do they explicitly bind rows to ownership/tenancy, or do they just check auth exists?
- Write paths (INSERT/UPDATE): are there equivalent ownership constraints for writes as for reads?
- Schema evolution: when new columns/tables are added, are policies revisited or assumed to still be correct?
How to detect Ownership-bound RLS Policies issues (signals + checks)
Use this as a quick checklist to validate your current state:
- Try the same queries your frontend can run (anon/authenticated). If sensitive rows come back, you have exposure.
- Verify RLS is enabled and (for sensitive tables) forced.
- List policies and look for conditions that don’t bind rows to a user or tenant.
- Audit grants to
anon/authenticatedon sensitive tables and functions. - Policy checks login, not ownership: Login checks don’t stop cross-user scraping.
- Policy checks login, not ownership: Policies must bind rows to ownership or tenancy.
- Policy checks login, not ownership: Direct API tests reveal leaks the UI hides.
- Re-test after every migration that touches security-critical tables or functions.
How to fix Ownership-bound RLS Policies (backend-only + zero-policy posture)
Mockly’s safest default is backend-only access: the browser should not query tables, call RPC, or access Storage directly.
- Decide which operations must remain client-side (often: none for sensitive resources).
- Create server endpoints (API routes or server actions) for required reads/writes.
- Apply hardening SQL: enable+force RLS where relevant, remove broad policies, and revoke grants from client roles.
- Generate signed URLs for private Storage downloads on the server only.
- Re-run a scan and confirm the issue disappears.
- Add a regression check to your release process so drift doesn’t reintroduce exposure. Fixes that worked in linked incidents:
- Policy checks login, not ownership: Adopt backend-only access or rewrite the policy to enforce ownership and tenant membership, then test direct API access from multiple users.
- Write policy missing WITH CHECK: Move writes behind backend endpoints that enforce ownership and input validation, and mirror the read policies with proper WITH CHECK clauses.
Verification checklist for Ownership-bound RLS Policies
- Test as two different users/tenants and attempt to read each other’s rows via direct API calls.
- Test writes by attempting to insert/update rows with someone else’s ownership identifiers.
- If policies are complex, move the operation behind a backend endpoint and verify behavior is correct end-to-end.
- Policy checks login, not ownership: Login checks don’t stop cross-user scraping.
- Policy checks login, not ownership: Policies must bind rows to ownership or tenancy.
- Policy checks login, not ownership: Direct API tests reveal leaks the UI hides.
- Policy checks login, not ownership: Backend-only access is safer when rules get complex.
- Write policy missing WITH CHECK: Write paths can be riskier than reads.
SQL sanity checks for Ownership-bound RLS Policies (optional, but high signal)
If you prefer evidence over intuition, run a small set of SQL checks after each fix.
The goal is not to memorize catalog tables — it’s to make sure the access boundary you intended is the one Postgres actually enforces:
- Confirm RLS is enabled (and forced where appropriate) for tables tied to this term.
- List policies and read them as plain language: who can do what, under what condition?
- Audit grants for anon/authenticated and PUBLIC on the tables, views, and functions involved.
- If Storage is involved: review bucket privacy and policies for listing/reads.
- If RPC is involved: review EXECUTE grants for functions and whether privileged functions are server-only.
Pair these checks with a direct API access test using client credentials. When both agree, you can ship the fix with confidence.
Over time, keep a small “query pack” for the checks you trust and run it after every migration. That’s how you prevent quiet regressions.
Prevent Ownership-bound RLS Policies drift (so it doesn’t come back)
- Prefer backend-only access when rules are non-trivial; it’s easier to test and audit than a policy web.
- Add explicit tests or checklist queries that fail if rows can cross the ownership boundary.
- Review policies on every schema change affecting ownership/tenancy fields.
- Keep one reusable verification test for “Policy checks login, not ownership” and rerun it after every migration that touches this surface.
- Keep one reusable verification test for “Write policy missing WITH CHECK” and rerun it after every migration that touches this surface.
Rollout plan for Ownership-bound RLS Policies fixes (without breaking production)
Many hardening changes fail because teams revoke direct access first and only later discover missing backend paths.
Use this sequence to reduce both risk and outage pressure:
- Implement and verify the backend endpoint or server action before permission changes.
- Switch clients to that backend path behind a feature flag when possible.
- Then revoke direct client access (broad grants, permissive policies, public bucket reads, or broad EXECUTE).
- Run direct-access denial tests and confirm authorized backend flows still succeed.
- Re-scan after deployment and again after the next migration.
This turns security fixes into repeatable rollout mechanics instead of one-off emergency changes.
Incident breakdowns for Ownership-bound RLS Policies (real scenarios)
Policy checks login, not ownership
Scenario: A marketplace app stores all customer data in one orders table and only checks that auth.uid() exists.
What failed: The policy is effectively “true for any logged-in user”, so attackers can enumerate rows for every customer.
What fixed it: Adopt backend-only access or rewrite the policy to enforce ownership and tenant membership, then test direct API access from multiple users.
Why the fix worked: Ownership binding makes the authorization invariant simple: you can only see rows you own.
Key takeaways:
- Login checks don’t stop cross-user scraping.
- Policies must bind rows to ownership or tenancy.
- Direct API tests reveal leaks the UI hides.
- Backend-only access is safer when rules get complex.
Read full example: Policy checks login, not ownership
Write policy missing WITH CHECK
Scenario: The team focused on SELECT and added a restrictive policy, but writes still happen directly from the client.
What failed: Users could insert or update rows with another user_id or tenant_id, letting them impersonate or escalate privileges.
What fixed it: Move writes behind backend endpoints that enforce ownership and input validation, and mirror the read policies with proper WITH CHECK clauses.
Why the fix worked: Backend writes let you enforce business rules and log attempts, which is harder to do in scattered policies.
Key takeaways:
- Write paths can be riskier than reads.
- Ownership must be enforced on inserts and updates.
- Backend endpoints enable validation and auditing.
- Test both read and write exposure, not just SELECT.
Read full example: Write policy missing WITH CHECK
Real-world examples of Ownership-bound RLS Policies (and why they work)
- Policy checks login, not ownership — The policy verified login but never bound rows to ownership, so attackers could scrape the table.
- Write policy missing WITH CHECK — Writes bypassed ownership controls because the WITH CHECK clause was missing.
Related terms
- Over-permissive RLS Policies →
/glossary/over-permissive-rls-policies - Row Level Security (RLS) →
/glossary/row-level-security
FAQ
Is Ownership-bound RLS Policies enough to secure my Supabase app?
It’s necessary, but not sufficient. You also need correct grants, secure Storage/RPC settings, and a backend-only access model for sensitive operations.
What’s the quickest way to reduce risk with Ownership-bound RLS Policies?
Remove direct client access to sensitive resources, enable/force RLS where appropriate, and verify via a repeatable checklist that anon/authenticated cannot query what they shouldn’t.
How do I verify the fix is real (not just a UI change)?
Attempt direct API queries using the same client credentials your app ships. If the database denies access (401/403) and your backend endpoints still work, your fix is effective.
Next step
Want a quick exposure report for your own project? Run a scan in Mockly to find public tables, storage buckets, and RPC functions — then apply fixes with verification steps.
Explore related pages
cross
Remove over-permissive RLS policies (adopt deny-by-default)/templates/access-control/remove-over-permissive-policies
cross
Policy checks login, not ownership/examples/ownership-bound-rls-policies/policy-checks-login-not-ownership