August 22, 2026
The Four-Step Takeover I Found in a Lovable App
A public table, a document parser, an AI generator and an open storage policy. Each feature worked. Together, they gave any free account…

By jacob perks
6 min read
- 1 A public table, a document parser, an AI generator and an open storage policy. Each feature worked. Together, they gave any free account control of every user's bot.
- 2 Step one: the public catalogue exposed the targets
- 3 Step two: the document parser checked the login, not the file
- 4 Step three: the generator would rewrite any bot
- 5 Step four: the storage policy allowed public deletion
A public table, a document parser, an AI generator and an open storage policy. Each feature worked. Together, they gave any free account control of every user's bot.
I did not need an admin account, a leaked password or an obscure exploit to take over another user's AI bot.
I needed a free account and one identifier.
The application was a listing platform built with Lovable. Each customer could create an AI bot, upload documents to teach it about a listing, and publish it so visitors can use it. The product worked. Authentication worked. Files were uploaded, PDFs were processed, and the bots answered questions based on the material provided.
The security review found 22 issues, including five critical ones. The number was not the interesting part. Four of the findings formed a complete takeover chain:
-
Find every bot and its identifier.
-
Read the private documents behind any one of them.
-
Replace its AI knowledge base.
-
Delete its source files.
The first two steps required no account at all. The other two worked from a free account.
I stopped at controlled proof using test records. I have removed the company name, sector, endpoint names and storage paths because the lesson does not depend on knowing the target.
Step one: the public catalogue exposed the targets
The application needed a public catalogue so visitors could discover published bots. The database policy went further. It allowed anyone to read every row in the bots table.
That included unpublished and inactive bots. It also included internal identifiers, owner contact details, configuration, custom prompts and the generated context used by the AI.
No login was required.
The page itself showed only the fields needed for the catalogue. That made the feature look harmless in the browser. The database returned the whole row and trusted the frontend to hide the rest.
An attacker does not have to use the frontend.
A direct request to the database returned the identifiers for every bot in the system. Those identifiers were the map for the next three steps.
This is the first pattern I see repeatedly in AI-built applications: a public feature is treated as permission to expose the underlying record. The requirement was "let visitors see active listings." The policy that shipped was closer to "let the internet read every bot."
Those are not the same rule.
Step two: the document parser checked the login, not the file
Customers could upload PDFs containing the material their bot should know: reports, maintenance records and other private documents. An edge function downloaded a file from storage, extracted its text, and returned the result to the application.
The function verified that the caller was logged in. That part was correct.
It then accepted a storage path from the request and downloaded that file using an administrator-level service key. Whether the file belonged to the caller was never checked.
Once I had another bot's identifier from the public table, I could derive its document path, pass that path to the parser, and receive the extracted contents while signed in to my own free account.
The function answered one question:
Is this person a user?
It never answered the question that mattered:
Does this file belong to this user?
That distinction is authentication against authorization. Authentication proves who made the request. Authorization decides whether that person may act on this particular object.
An application can have perfectly functioning login and still have no meaningful access control after the login screen.
Step three: the generator would rewrite any bot
The platform also had a function that regenerated a bot's knowledge base when its owner changed the listing or uploaded new material.
The function required a valid login, accepted a bot identifier from the request, and used an administrator-level database client to do the work. The same pattern from the document parser had been repeated.
It fetched the requested bot and rebuilt its generated context without comparing the bot's owner to the signed-in user.
Any free account could therefore submit another customer's identifier, thereby replacing that bot's knowledge base.
The victim would not receive an error. Their listing would stay online, and the chat interface would keep working. It would simply start answering from material supplied by someone else.
That is more dangerous than a visible outage. A broken bot gets investigated. A bot that confidently gives altered answers may stay live for days.
This is the second recurring pattern: administrator credentials inside a server function are treated as proof that the operation is safe. The credentials only prove that the function can bypass the database rules. They say nothing about whether the person calling the function should be allowed to do so.
Every function that uses a service key has to rebuild its own authorization. If it does not, the login check is theatre.
Step four: the storage policy allowed public deletion
The final step did not require an account.
The storage bucket allowed public uploads, reads and deletes. There was no ownership condition and no folder restriction. Anyone who knew a path could remove the object at that path.
The public table supplied the bot identifiers. The document workflow revealed how the paths were structured. The storage API handled the deletion.
That meant an unauthenticated request could remove another customer's listing images and uploaded documents.
At this point the chain was complete:
- The public database policy identified every target.
- The document function traded a target's path for its private contents.
- The generator traded the same target's identifier for write access to its AI behaviour.
- The storage policy traded the path for permanent deletion.
Discover, read, overwrite, destroy.
None of it required breaking the login system.
Every feature worked exactly as designed
The catalogue returned listings. The parser extracted PDFs. The generator rebuilt bots. The storage bucket accepted file operations.
Each feature passed its local test.
The vulnerability existed in the relationship between them.
This is where fast, AI-assisted development is weakest. An AI coding tool is good at satisfying the request immediately in front of it:
- Make these listings public.
- Let a signed-in user process a PDF.
- Regenerate a bot when its content changes.
- Let the app upload and remove files.
The tool does not automatically preserve an unwritten rule across every feature: a user may act only on bots and files they own.
That rule has to survive database policies, edge functions, storage paths and administrator clients. If it disappears in one place, the other layers may hide the mistake rather than catch it.
In this application it disappeared four times.
Why ordinary testing does not reveal it
The owner signs in, opens their own bot, uploads their own document and regenerates their own knowledge base. Every request succeeds because the application is supposed to allow all of those actions.
Testing a single account cannot determine whether account A can access account B's resources.
The smallest useful authorization test needs two accounts:
-
Create a bot and upload a document as account A.
-
Copy the bot identifier and document path.
-
Sign in as account B.
-
Repeat every read, update and delete request using account A's identifiers.
Every request should fail with a permission error. A successful response is the finding.
This is also why a generic scanner may report four separate configuration issues without explaining the takeover. A public table, an ownership check and a storage policy are detectable in isolation. Understanding that one response supplies the identifier required by the next endpoint depends on what those objects mean inside the product.
The chain is a product fact, not only a code fact.
One fix breaks the chain, but four fixes close it
Any single repair would have interrupted the full takeover. That is not a reason to stop after one.
The database policy should expose only active public records, with a separate rule allowing owners to see their own drafts. Private fields should not live in the public response just because the frontend does not render them.
The document function should not accept an arbitrary storage path and trust it. It should load the document record through the signed-in user's ownership relationship, derive the path on the server, and reject anything outside that scope.
The generator should query using both the bot identifier and the owner identifier. A record that does not match both should look nonexistent to the caller. Checking ownership after an administrator client has already updated the record is too late.
The storage bucket should scope writes and deletes to authenticated owners using a consistent folder structure. Public listing images may need public reads. Public reads do not imply public deletes.
Then the two-account test should be repeated against every step.
This is defence-in-depth in its least glamorous form. Four small checks enforce the same ownership rule in four different places. The repetition is the point.
The hidden boundary is the product
People often ask whether Lovable, Supabase or another part of the stack is secure.
All three can be used securely. That was not the question this review exposed.
The missing information was specific to the product: which user owns which bot, which documents belong to that bot, which parts of a listing are public, and which operations must remain private even when the resulting page is public.
No framework can infer all of that from a login screen. No AI builder can preserve rules it was never given. No scanner can decide ownership from a successful HTTP response alone.
The application worked because each feature knew how to do its job. It was exposed because none of them knew where the user's authority ended.