Short answer
Many apps made with browser app builders let the browser query a hosted database directly, using a key that is public by design. Anyone can copy that key from your page, so what protects your data is the database’s row level security: rules that decide, row by row, who may read or change what. Turn row level security on for every table the browser can reach, write one policy per action that ties each row to the signed in user, keep any service key on the server, and test the result by trying to read another user’s data.
Why are browser-built apps different?
A traditional web app puts a server between the browser and the database. The browser asks the server, the server checks who is asking, and only then touches the data. If the check is missing, that is a bug in one place you can read.
Browser builders often skip that middle layer to get you to a working app faster. The frontend talks to a hosted backend directly, carrying a public key that identifies your project. The key is meant to be visible: it ships in the page, and anyone who opens the developer tools can read it. That is fine, as long as the database itself refuses requests that should not succeed. If it does not, the public key plus your table names is all an attacker needs.
So the security of these apps comes down to one question that is easy to skip: what does the database allow someone holding only the public key to do?
What went wrong in CVE-2025-48757?
The public record of this risk is CVE-2025-48757, published in May 2025. Its description reads, in part: “An insufficient database Row-Level Security policy in Lovable through 2025-04-15 allows remote unauthenticated attackers to read or write to arbitrary database tables of generated sites.” The record is rated critical and is classed as incorrect authorization (CWE-863).
The record is also marked disputed. The supplier’s position, quoted in the same record, is that “each individual customer of the Lovable platform accepts a responsibility over protecting the data of their application.” Whichever way you read the dispute, the practical lesson for a founder is identical: the rules on your database are yours to get right, whichever tool generated them. No builder can know your rules unless you state them.
How does row level security work?
Row level security is a feature of Postgres, the database underneath many hosted backends. When it is enabled on a table, every query is filtered through policies you write. The PostgreSQL documentation states the default plainly: “If no policy exists for the table, a default-deny policy is used, meaning that no rows are visible or can be modified.”
That default is your friend. A table with row level security on and no policies is closed. The danger is the opposite case: a table with row level security switched off, which is open to anyone holding the public key, or a table with a policy written to make an error go away, such as one that allows every row to everyone.
Two details trip people up. First, policies are per action: reading (SELECT), adding (INSERT), changing (UPDATE) and removing (DELETE) can each have their own rule, and a table can be safe to read and unsafe to update. Second, the documentation notes that table owners and roles with the bypass attribute skip row security entirely, which is why the service key that your backend uses must never reach the browser.
What should the policies look like?
Most tables in a small app fall into one of a few shapes. Name the shape for each table before you write anything:
- Owned by one user, such as profiles, notes or invoices. Each action allowed only where the row’s owner column equals the signed in user’s id.
- Shared within a team or workspace. Allowed where the signed in user is a member of the row’s team, checked against a membership table that itself has a policy.
- Public to read, private to write, such as published posts. Reading open to everyone; adding, changing and removing limited to the owner.
- Server only, such as payments, audit logs or anything an admin manages. Row level security on with no policies for the browser at all, and the server uses its own key.
For updates, check both sides. The rule that decides which rows a user may update (USING) is separate from the rule that checks what the row looks like afterwards (WITH CHECK). Without the second, a user can update their own row and set the owner column to someone else, or mark their own account as an admin.
Ask your coding tool to write the policies, then read them yourself. A good prompt: “For every table in the database, tell me whether row level security is enabled, list each policy with its action and condition, and flag any policy whose condition is always true or does not mention the signed in user. Do not change anything yet.”
How do you test the rules as a stranger?
A policy you have not tested is a guess. The test is simple and takes about half an hour:
- Create two accounts, A and B. Add some data as B.
- Signed in as A, try to read B’s rows by id, list B’s rows, update one and delete one. Every attempt should fail or return nothing.
- Signed out, using only the public key from your page, try the same reads and writes. Every attempt should fail.
- Try to update your own row as A while changing its owner column or an admin flag. It should be rejected.
- Check file storage the same way: can A open a file B uploaded by guessing or copying its path?
Write the results down with the date. If one of these succeeds, that is a critical finding: fix the policy, then run the same test again to confirm. The guide lock down a hosted database has the full sequence with paste-in prompts.
What else should you check on these builders?
- The service key stays on the server. Search the built frontend for it. If it was ever shipped, treat it as leaked and rotate it.
- Server functions check the caller. A server function that reads a record by id needs the same ownership check as any API route; see IDOR in AI-built apps.
- Storage buckets are private unless they must be public, and uploads are limited by type and size.
- Apps with their own server code, which some builders generate, need the route checks as well as the database rules. The rules are a second layer, not a replacement.
- A generated interface is only the front half. Whatever backend you connect it to needs the same review.
Where should you start today?
Open the list of tables, confirm row level security is on for each one the browser can reach, and run the two account test. Those two steps close the failure described in the public record above, and they cost an afternoon.
Then work through the rest of the free Vibe Coding Security Checklist, which lists the database checks alongside access control, secrets, headers and AI features, each ranked by severity with a fix deadline.
Sources
- CVE-2025-48757, CVE Program
- Row Security Policies, PostgreSQL documentation
- A01:2025 Broken Access Control, OWASP Top 10:2025