In this piece
Lovable data exposure: how to prove a permissions fix
You have changed a database policy after a security warning. Your own account still works. Before closing the finding, check whether another customer can read your private records and whether a signed-out request gets anything it should not.
To verify a Lovable data-exposure fix, define the intended permissions, test allowed and forbidden operations with synthetic records, then repeat those checks against the deployed data paths using real application identities. Keep the before-and-after evidence. A passing local policy test supports a specific rule; production access requires its own retest.
Research status: 4 November 2026. The incident figures below are attributed to their researchers. Our separate example uses synthetic data and local PostgreSQL policies. The hosted application checks are a proposed procedure for an environment you own or are authorized to assess.
Independence and trademarks: Wavect publishes this page and is itself a provider, so we have a commercial interest in it. We are not affiliated with, endorsed by or partnered with the other companies named here, and all third-party company names, brands and trademarks are the property of their respective owners. Statements about other providers are taken from publicly available sources, primarily their own published pages, as of the review date shown on this page, and may have changed since. Please verify them directly before you decide. This page was written to the best of our knowledge and with the intent to remain objective. If you believe anything here is inaccurate or unfair, write to us and we will correct it: [email protected]
What does the Lovable data-exposure report establish?
Crissy Field’s November report describes unauthenticated reads in selected Lovable-built applications. It identified projects in Common Crawl archive records dated December 2025 to September 2026; the figures update the Handelsblatt story of 2 November. The counts describe different subsets.
| Stage | Count |
|---|---|
| Identified projects | 159,041 |
| Using Supabase | 34,545 |
| Returning records on page load | 24,664 |
| Sites with email-containing records in further reads | 7,403, across 10,560 tables |
The reported 30% is 7,403 of 24,664, not a confirmed leak rate. Email matches can include public contacts or test data; the sample is not representative. Lovable’s own database offering was excluded.
Examples include over 200,000 messages from one German lifestyle forum, more than 1,000 identity-detail records including passport numbers at one service provider, and payment information in sales records. The report does not specify payment-card fields or establish misuse. These findings do not identify your application’s exposure or current repair status.
Is this the same as Lovable’s earlier chat-history incident?
Lovable’s April chat-history incident concerned access to the builder’s project, rather than the deployed application’s database. In its 22 April incident statement, Lovable describes a regression from 3 February to 20 April: authenticated Lovable users with a public-project link could access its source and chat history.
Lovable says it fixed that regression and that private projects and Lovable Cloud were unaffected by it. That statement does not verify your deployed application’s permissions.
| Boundary | Question to answer | Useful evidence |
|---|---|---|
| Editor project | Who can open source, builder conversations and unpublished work? | Project membership and visibility settings |
| Published website | Who can reach the deployed pages? | Published-access configuration and a visit under the intended identity |
| Application data | Which records and operations can that caller reach? | Actual backend responses and resulting database state |
Our Lovable application due-diligence guide covers the wider ownership and readiness review. This retest addresses a narrower decision: whether a defined data-access finding is ready to close.
Why can a login screen leave database records readable?
A login screen controls the application’s interface; the backend must enforce which data each request can reach. In a browser-connected Supabase app, hiding a page or filtering a list in JavaScript does not establish database authorization. Test the data request itself, including its behavior without a signed-in user.
The Supabase API-key guide distinguishes a publishable key from a user’s identity. A publishable key belongs in public clients. With no user session, requests use anon; a signed-in session uses authenticated. Secret keys and legacy service_role credentials require separate handling because elevated access bypasses RLS.
Row-level security is PostgreSQL’s mechanism for deciding which rows an operation may access. Supabase’s RLS guide separates table grants from row policies: grants permit an operation on the table; policies constrain its rows. Inspect both. Removing a frontend button does not change either permission.
For example, a list query that includes the current user’s ID can display exactly the right rows while a broader permitted query exposes more. Our recommended check changes the requested record while keeping the caller unchanged. The backend should enforce the ownership rule even when the interface would never offer that request.
What should the permission test prove?
A permission test should prove that approved work succeeds and prohibited access fails under a known identity. Begin with two synthetic users and separate private records. Define their permitted fields and operations first; a test can faithfully confirm a policy that does not match the product’s intended behavior.
| Caller | Read | Allowed change | Must be rejected |
|---|---|---|---|
| Signed-out visitor | No private rows | None on this private table | Reading, creating or modifying private records |
| User A | A’s permitted fields | A’s approved editable fields | Reading B’s row, writing as B or transferring ownership |
| User B | B’s permitted fields | B’s approved editable fields | The corresponding operations against A |
| Backend task | Only its documented job scope | Only its documented job scope | Acting on an unverified caller’s arbitrary record request |
Add intentional public content as a separate positive case. A public catalogue should still be readable if that is its purpose. For private data, prove the owner can read the seeded record immediately before checking that another user cannot. An empty table or broken environment can otherwise make every denial assertion look successful.
Also identify guest accounts explicitly. Supabase anonymous sign-ins create Auth users with the authenticated role and an is_anonymous JWT claim. If your requirement is permanent accounts only, that role alone does not express it. Add the relevant policy condition and test identity.
For teams and organizations, replace the simple owner rule with the actual membership and role rules. Include a user whose membership was removed. Decide which background tasks remain authorized after removal, then test that decision instead of assuming all access should disappear in the same way.
What did our synthetic PostgreSQL test find?
Our synthetic PostgreSQL test reproduced exposed reads, two incomplete repairs and a policy constrained by owner and tenant. All 23 recorded checks passed against their expected outcomes. That includes confirming the deliberately insecure states. The corrected state preserved permitted work while rejecting the tested cross-user and cross-tenant operations.
We ran Node.js 24.22.1 and @electric-sql/pglite 0.5.15, which reported PostgreSQL 18.6. PGlite provides an in-memory PostgreSQL runtime. The fixture seeds three notes: Alice and Bob share tenant A; Carol belongs to tenant B. Each user owns one note.
| Policy state | Signed-out rows | Alice’s rows | Interpretation |
|---|---|---|---|
| RLS disabled, read grants present | 3 | 3 | The private-record boundary fails. |
| RLS enabled, no policies | 0 | 0 | The denial passes, but the permitted owner cannot read. |
Authenticated read policy with USING (true) | 0 | 3 | Blocking signed-out reads leaves other users' records exposed. |
| Owner and tenant conditions | 0 | 1, exactly alice-note | The owner read succeeds without returning Bob’s or Carol’s note. |
The driver asserts the effective PostgreSQL role, user ID and tenant ID before each operation. Bob and Carol each read exactly their own seeded note. Alice’s identity paired with the wrong tenant reads zero rows. These positive controls distinguish a working restriction from an empty or inaccessible database.
This is the exact policy excerpt from the fixture. The fixture.uid() and fixture.tenant_id() helpers read test-controlled SQL settings. They are local identity substitutes, not Supabase Auth functions or JWT verification. This excerpt is not a production migration; trusted tenant membership remains untested.
DROP POLICY signed_in_read ON public.notes;
CREATE POLICY private_notes ON public.notes FOR ALL TO authenticated
USING (owner_id = fixture.uid() AND tenant_id = fixture.tenant_id())
WITH CHECK (owner_id = fixture.uid() AND tenant_id = fixture.tenant_id());Alice’s allowed update, insert and delete each returned one expected record. Updating Bob’s or Carol’s note, or deleting Bob’s note, returned zero rows. Attempts to change ownership, change tenant or insert a note for Bob raised SQLSTATE 42501. After each denied write, the driver compared the full table snapshot and confirmed it was unchanged.
A separate BYPASSRLS role read all three records despite carrying Alice’s test identity. We also deliberately disabled RLS again: the owner assertion detected the extra rows, and restoring RLS restored the one-row result. The regression therefore responds to a real policy change.
Download the policy regression fixture and recorded results. Extract it, then run:
npm ci --ignore-scripts
npm testThe package includes the dependency lock and results.json. It tests PostgreSQL grants and policies with controlled local identities. It does not test Supabase Auth, JWT verification, PostgREST, hosted HTTP responses, views, storage or Edge Functions. The deployed checks below remain necessary for your application’s actual scope.
Why can an RLS-enabled fix still fail?
An RLS-enabled table can still grant access that violates the intended rule. Inspect every applicable policy and privilege, then test alternate data paths and returned fields. Preserve permitted operations while constraining the new values a write can store.
PostgreSQL’s row-policy reference explains that permissive policies combine with OR. Adding a narrow owner policy while retaining an applicable allow-all policy does not narrow the result. The same reference explains why superusers, roles with BYPASSRLS and, normally, table owners are unsuitable stand-ins for ordinary callers.
Rows and fields need separate decisions. The Supabase column-privilege guide notes that RLS does not control which columns are accessible within a permitted row. We would separate public-profile fields from internal records or use deliberately designed column permissions. Check for table-level grants that still permit the columns you intended to restrict.
Privileges can also change across a function boundary. Supabase documents that a security definer database function runs with its owner’s privileges. Restricting who may execute it does not automatically restrict which user’s rows it returns. Review the function body and test its caller-facing result.
Do not broaden a policy to make a failing screen work. First identify whether the failure comes from a missing grant, an ownership condition, an incorrect identity or the wrong deployment. Repairing the actual cause keeps the permitted workflow working without reopening the original access path.
How do you retest the deployed application?
Retest the deployed application through each data path included in the finding, using real application identities and synthetic records. Record the response and any persistent change. This also exercises session handling, configuration and the code currently serving users.
Supabase’s testing overview distinguishes database tests from application-level tests through the client your app uses. Carry the same allow-and-deny cases across both. A migration in Git is useful evidence of intent; confirm that the target environment has applied it.
- Identify the target. Record the project, deployment revision, migration and approved routes. Use separate browser profiles or clients for A, B and signed-out requests so a retained session does not silently change the caller.
- Seed controlled records. Give A and B unmistakably different synthetic values. Keep a permitted read that proves the target record exists and the route works. Avoid collecting unrelated customer data as evidence.
- Exercise reads and writes. Test lists, direct-record requests and the actual create, update and delete actions in scope. Check returned fields. After a rejected write, confirm that the original row remains unchanged.
- Retain the result. Save sanitized requests, responses, effective identity, row counts and the before/after state. Record missing or expired-session behavior separately from the valid-but-wrong-user case.
| Path | Reason for a separate check | Evidence to retain |
|---|---|---|
| Database view | View ownership and security_invoker affect whose underlying permissions apply. | A/B results through the view as well as the base table |
| File storage | Object policies govern storage operations, but public buckets permit public downloads. | The intended list, download and upload behavior for controlled objects |
| Edge Function | Function authentication and client choice determine whether queries use caller-scoped or elevated access. | Verified caller, authorized action and returned record scope |
| Export, background job or subscription | The product may provide another route to the same records. | The corresponding ownership check and output under the agreed scope |
Include the paths your application exposes and leave untested ones open. Our vibe-coded software audit guide explains how to turn such a boundary into an agreed assessment scope and deliverable.
What does Lovable’s security scan cover?
Lovable’s Quick and Deep security scans inspect code and configuration with different coverage. Use their findings alongside your permission matrix. A scan cannot define every business rule or demonstrate that your deployed A/B test was executed.
The current security overview says Quick checks database access settings, known dependency vulnerabilities and unauthenticated MCP exposure during publishing. Deep adds application-code analysis and includes Quick. The built-in scans analyze code and configuration; optional runtime penetration testing is separate. Deep can run on demand, through Enterprise schedules or when publishing an app with unauthenticated MCP access.
The project Security view ties findings to a code version. Refresh stale scans and inspect ignored findings, which are excluded from the open count. Its “No issues found” message means the latest scan found nothing; the documentation explicitly says scanners cannot catch every risk. Blocking publication on critical findings depends on workspace controls.
Keep the scan timestamp and reviewed findings beside the retest evidence. Inspect automated policy changes and rerun the regression. Apply the same acceptance criteria to changes written by people and assistants.
What if confidential data was already readable?
If your own application returns confidential data to an unauthorized caller, contain the specific access path and involve the people responsible for security and data protection. We recommend separating immediate containment from the investigation: restricting access can stop further reads, while determining historical access requires evidence that a clean retest cannot supply.
Preserve the relevant configuration, deployment timeline and available logs before making changes that would destroy useful context. Document which records and operations the finding concerned. Keep sensitive evidence in a controlled location; the issue tracker should normally contain synthetic reproductions or redacted material.
Choose containment with the system owner: restrict an exposed function, remove an unintended grant, pause the affected feature or correct its policy. If an elevated credential was exposed, retire that credential through the provider’s rotation procedure as well as removing the exposure. Treat a public publishable key according to its intended role.
Do not infer historical safety from an empty response after the fix. The evidence you retain should distinguish when the issue was observed, when containment took effect and when the deployed retest passed. The responsible incident team can then assess notification and other obligations against the actual facts.
When is the permission fix ready to close?
Close a permission finding when its defined allow-and-deny cases pass in the deployed scope, permitted workflows still work, and the evidence identifies the tested change and callers. We would leave the finding open when the only evidence is enabled RLS, a successful administrator query or a green scan with no application-level retest.
| Record | What to capture |
|---|---|
| Intended boundary | A can read and edit approved fields of A’s private record; B and signed-out callers cannot. |
| Change tested | Deployment revision, database migration and relevant function or policy definitions |
| Observed behavior | Actual A/B/anon responses, returned fields, affected rows and unchanged state after rejected writes |
| Remaining scope | Untested paths, identity types, deployment differences and the person responsible for follow-up |
| Closure decision | Reviewer, test date and the exact finding closed, without an application-wide safety claim |
The Cedars assessment case describes our work on documentation, QA and bounded technical assessment. Agree the required evidence before work starts. Our penetration-testing service can scope an authorized assessment and retest of the running application.
Use the software release QA checklist for the broader launch decision. For this finding, require the permitted request to succeed and the prohibited request to fail against the same deployed revision. Retain both results before closing it.
