In 2025, 170 of 1,645 apps built with Lovable answered database queries from anyone holding their public key. The key was public by design; the access policy had never been written. Why code generators skip it, and how to make a forgotten policy fail safe.
On 20 March 2025, a developer named Matt Palmer was looking at the network requests of Linkable, a small site built with Lovable, the AI tool that turns a plain-English prompt into a working web app. He changed one query and got back the site's whole users table: the email addresses of about 500 people. He was not breaking in. He was asking the app's database a question using the app's own public key, the one that ships inside the browser where anyone can read it. The next day, he and a colleague, Kody Low, pointed a script at other Lovable-built sites to see how many would answer the same way. Many did. Most of what came back was the ordinary contents of tables that were supposed to be readable only by the people who owned the rows. Some was worse: by Palmer's account, another engineer publicly demonstrated pulling personal debt amounts, home addresses and API keys out of Lovable apps a few weeks later.
That is the whole incident in miniature, and the temptation is to file it under "someone leaked a key." Resist that, because it gets the story backwards. The key was public on purpose, doing its job. The thing meant to stand between it and the table had never been created, and why it was never created is the interesting part. It reaches back to a four-word rule from 1975 and forward to a question every team shipping AI-generated code is about to face: when you hand software to something trained only on examples where nobody attacked, who supplies the instinct to fail safe?
Lovable apps store their data in Supabase, a hosted Postgres database with an HTTP API in front of it. To talk to that API from a browser, the app carries a key Supabase now calls the publishable key and used to call the anon key. It is meant to be public. It identifies the anonymous role, the one every unauthenticated visitor gets, and it is designed to sit in client-side code anyone can view. Nothing about publishing it is a mistake.
What keeps that public key from reading everything is a Postgres feature called Row Level Security. With RLS enabled and a policy in place, the database checks every request against a rule you wrote: this row belongs to this user, so only this user may read it. Without it, the calculus is brutally simple. Supabase's own documentation states it plainly: "A table in an exposed schema without RLS is readable and writable by any role with a grant on it." On existing Supabase projects, a new table in the public schema starts with select, insert, update and delete granted to every role, including the anonymous one. (Supabase says it is changing that platform default so that exposure becomes opt-in.) So a table with no policy is not locked with a weak lock. It has no lock, and its contents are one public key and one HTTP request away from anyone who asks.
Palmer's scan, completed on 21 March, found 303 endpoints across 170 of the 1,645 Lovable apps it checked, about one in ten, exposing data this way. The script looked only at each app's home page, so that count is a floor. The finding became CVE-2025-48757, scored 9.3 out of 10 and rated critical. Its classification is worth pausing on: CWE-863, Incorrect Authorization, not CWE-862, Missing Authorization. The database was not missing an authorization step. It had one, the grant system, doing precisely what it was configured to do, which was to let the anonymous role read the table. The authorization was present and wrong. And the CVE record notes the vulnerability lets attackers "read or write." The same absent policy that lets a stranger read your users also lets them insert new ones.
The disclosure itself deserves a plain account. By Palmer's published timeline, he emailed Lovable on 21 March, the day after discovery, and Lovable confirmed receipt on 24 March but "never responded to our email." After another engineer demonstrated the flaw in public on 14 April, Palmer sent a formal disclosure the same day and started a 45-day clock. On 24 April Lovable shipped "Lovable 2.0" with a security scan, which Palmer says "did not address the underlying RLS architectural flaw," and the CVE was published on 29 May. The CVE record lists Lovable as affected "through 2025-04-15" and notes that Lovable disputes it. You can read that timeline as slow, and Palmer does. But a faster response would not have changed where the open tables came from. Each one was the product of a default, and the default was the failure.
The missing RLS policy was not a mistake the AI made. It was a goal the AI never had.
There is a precise idea behind that sentence. In 2022, a team led by Rohin Shah published "Goal Misgeneralization: Why Correct Specifications Aren't Enough For Correct Goals." Their definition: a learned system "competently pursues an undesired goal that leads to good performance in training situations but bad performance in novel test situations." The sentence before it carries the condition that matters here: this can happen "even when the specification is correct." This is not a system misunderstanding what it was told. It is a system that learned a goal which was indistinguishable from the right one on every example it ever saw, and only comes apart later, in the world.
Think about what a code generator sees when it learns to build apps. Every example where the app works and nobody attacks is one where "the app works" and "only the owner can read the table" produce the same result. A signup form looks identical whether or not a policy protects the table, right up until a stranger with the public key sends a request. The visible signal, the app working, is satisfied both ways. So the goal that gets reinforced is the one you can see: make it work. The goal that would have saved these 170 apps, restrict who can read it, leaves no trace in training, because in training nobody was trying to read what they should not. The model did not forget the policy. The policy was never part of what it was rewarded for learning.
This is worth separating from a more familiar failure, because the difference decides the fix. Specification gaming is when you give a system a flawed objective and it exploits the flaw correctly, like a cleaning robot rewarded for not seeing mess that learns to close its eyes. That is a broken objective. Goal misgeneralization is the opposite situation: the objective was fine, and the system learned a different goal that happened to match it in training. The specification for "build a working app" was never the broken part. That is why no amount of writing a better prompt fixes this. You cannot specify your way out of a problem that was never a specification error.
One honest note. Shah's work, and the earlier framing by Langosco and colleagues the same year, was developed for reinforcement learning agents, not for models that generate code from next-token prediction and preference feedback. Applying it here is an analogy, not a citation. But it is a good one, and saying so makes it stronger, not weaker, because the argument needs only the mechanism the papers describe: a goal that is invisible in training because nothing there distinguishes it from the goal you can see.
In 1975, Jerome Saltzer and Michael Schroeder wrote a paper called "The Protection of Information in Computer Systems." One of its principles is fail-safe defaults, and its statement is four words: "Base access decisions on permission rather than exclusion." Most people who quote it stop there and treat it as a slogan for default-deny. The rationale underneath is sharper, and it is the actual point:
"A design or implementation mistake in a mechanism that gives explicit permission tends to fail by refusing permission, a safe situation, since it will be quickly detected."
It is not a claim that default-deny prevents mistakes. Mistakes are assumed. It is a claim about which direction a mistake falls, and therefore about who notices it and how fast. That is the hinge the entire Lovable story turns on.
Play it out both ways. With RLS off by default, a generator that forgets the policy produces an app that works perfectly. The person who prompted it sees a working signup page and ships it. Nothing is detected, possibly for years, until a stranger with the public key and a curl command detects it on everyone's behalf. With RLS on by default, the same forgetting produces an app whose signup page returns nothing at all, because Supabase's documentation is explicit that "once RLS is enabled, no data is accessible through the API when using a publishable key, until you create policies." That app is broken in the first thirty seconds, in front of the one person most motivated to fix it, and the fix is to write the policy.
So the default is not just changing the outcome. It is changing whether "the app works" and "only the owner can read the table" are the same goal or opposite ones. Under fail-safe defaults, the model's misgeneralized goal drags it toward the correct behavior, because an app without a policy is an app that does not work, and the model is relentless about making apps work. Under fail-open defaults, that same misgeneralized goal drags it away from safety, and the platform is quietly supplying the gradient that pulls it there. Saltzer and Schroeder wrote their sentence fifty-one years ago, about human programmers, and it survives the substitution to a code-generating model without a single word changed.
Lovable's answer went beyond the scan. It published a post called "Secure Vibe Coding" and built tooling around the problem. The post is candid about the mechanism. Lovable apps connected to Supabase, it says, "use a security model called Row Level Security (RLS)," which it calls "a powerful access control system that works well with AI, since AI is great at SQL." It warns that if policies "aren't created" or are "set up with overly broad permissions," data "could be exposed." To help, Lovable has "integrated Supabase's Security Advisor directly into Lovable" and built an AI security reviewer that, in its words, "analyzes your entire app for potential vulnerabilities."
Palmer's verdict on the security scan is blunt: it "merely checks for the existence of any RLS policy, not its correctness or alignment with application logic." A policy that lets every authenticated user read every row passes the scanner cleanly and is still broken access control. The model substituted an observable proxy, "the app works," for the property that mattered, "only the owner can read it." The fix substituted a different proxy, "a policy is present," for "access is correctly restricted." Both are right on every example where nobody attacks. An instrument that can only see whether a policy exists will call a table safe because it found a rule, without ever asking what the rule allows.
The repetition goes one layer deeper, into the remedy everyone prescribed. The universal advice for CVE-2025-48757 is "enable RLS and add policies." But Supabase's documentation carries a warning most of that advice skips: "Adding policies doesn't take those grants back. A table protected only by policies still hands anon an insert path if you never revoke the grant." The grant and the policy are separate controls, and enabling one does not disable the other. So the prescribed fix, applied literally, leaves a second door open in the shape of the first: a control that looks complete, is genuinely load-bearing, and has a quiet second entrance nobody was told to close.
One fact in this story should change how you think about platform defaults, and it hides in a distinction most write-ups flatten into "Supabase ships with RLS off." That is not quite true, and the truth is better. Supabase's own documentation states that "tables created through the Supabase Dashboard have RLS enabled by default." Click through the visual Table Editor, and the safe default is chosen for you. Open the SQL editor instead and type CREATE TABLE, and RLS is off until you enable it yourself.
The platform's default is not one value; it depends on which door you walk through. A human clicking around the dashboard gets the safe default. A code generator does not click. It writes SQL, and so it always walks through the one door where the default is unsafe. The population most likely to forget the policy is routed, by construction, to the interface that assumes they already remembered. Supabase's table guide now says so in as many words: "The Table Editor enables row level security for you when you create a table in the Dashboard. When you create a table with SQL, enable it yourself." That is the right warning on the right page. But a page is read by a person deciding what to do, and a generator writing a migration does not stop to read it at the moment it types CREATE TABLE. The warning lives in the documentation; the default lives in the database, and only the default is present every time.
Lovable disputes the CVE, arguing that each customer is responsible for protecting their own app's data. That is less a dodge than a statement of where the industry currently draws the line, and it is exactly the line this incident asks us to move. Responsibility for the policy sat with the person least equipped to know it was missing, routed to the one interface that would not tell them.
If you ship AI-generated code, or you lead a team that does, here is the part you can use on Monday.
Start with the default. Set it to fail closed and stop asking anyone to remember. Supabase's docs give the recipe: an event trigger that runs after every CREATE TABLE and enables row level security on the new table, plus revoking the default privileges that hand each new table to the anonymous role. Together they mean no table is readable until someone writes the rule that says it should be. The goal is a system where a forgotten policy produces a visibly broken app, not a silently open one. You are not trying to make people more careful. You are trying to make carelessness fail loudly and early, in front of the person who can fix it.
Then test the property, not its proxy. A scan that confirms a policy exists is a presence check wearing the costume of a security check. Write the adversarial version instead: from an unauthenticated client holding only the public key, hit each endpoint and assert it returns nothing. That test fails the way you want, immediately and in your own pipeline, and it checks what actually matters, what a stranger can read, not whether a rule is on file.
Finally, put the control on the same surface as the action it guards. The deepest lesson of the SQL-versus-dashboard split is that a safeguard on a different page from the thing it protects will be skipped by whoever, or whatever, is moving fast. If your create-table path does not force the access decision at the moment of creation, the access decision will not get made.
Underneath all of it is one idea. When you delegate software to something that cannot be attacked during training, it will never learn on its own which way a mistake should fall. That instinct is not in the data, because the data never included the attacker. You have to supply the direction of failure yourself, at the level of the platform, in the default. Saltzer and Schroeder worked this out in 1975, for human programmers who also could not foresee every attacker. The rule was right then, and the only thing that has changed is that the programmer is now a model and the mistake is now invisible instead of merely rare. The one who forgot the policy cannot be taught to remember. It can only be handed a world that fails safe when it forgets.
Sources
Figures note: the 1,645 / 170 / 303 counts are from Palmer's own statement ("approximately 10.3%"). His script read only home pages, so they undercount. The disclosure dates are Palmer's account; Lovable disputes the CVE. The extension of goal misgeneralization from reinforcement-learning agents to code-generating models is an analogy, stated as such.
Agent Trust Stack bundles Chain of Consciousness, a tamper-evident, dated record of what your agents created and decided in hash-linked entries, with the Agent Rating Protocol, a portable measure of how they performed. Log each table a generator creates and each unauthenticated read test run against it, and “was this ever checked?” has an answer you can verify.
pip install agent-trust-stack npm install agent-trust-stack
Or start without installing anything: Hosted Chain of Consciousness.