Why can one of my users see another user's data even though everyone has to log in?
The answer
Because logging in and being allowed are two different checks. Authentication asks who you are; authorization asks what you may touch. AI coding tools reliably build the first and routinely skip the second, so a logged-in user who changes an id in a request often gets somebody else's record back.
By Muhammad Bilal13 min read
The short version
- Authentication and authorization are separate checks. Every AI coding tool builds the first well, because a broken login is visible in five seconds. A missing ownership check produces no error, no warning and no visible symptom at all.
- The proper names are worth knowing: broken object level authorization for the wrong row, broken object property level authorization for the wrong column, broken function level authorization for the wrong endpoint. Older writing calls the first one IDOR.
- Row-level security does not cover this on its own. Database functions, views, edge functions using the admin key, public storage buckets and roles read from user-editable metadata all sit outside it.
- The evidence is now specific rather than anecdotal: one 2026 study of AI-coded applications found authorization the second-largest category of confirmed flaws, and found it more than twice as common in the larger application as in the small ones.
- You can test for it yourself with two accounts and about ten minutes. You do not need tools, and you do not need to be able to read the code.
There is a sentence I hear more than any other when I ask whether an app is safe, and it is always said with real confidence: you have to log in to use it.
That is true, and it is worth something, and it is not the thing the person thinks it is. A login is a turnstile. It establishes that whoever is inside the building is somebody rather than nobody. It says nothing whatsoever about which rooms they are allowed to open once they are in there.
This is the gap that most AI-built applications ship with, it is the one that produces the worst outcomes, and it is almost invisible until somebody goes looking. This article is about what it is, why the tools produce it so reliably, and how to find out in about ten minutes whether your own app has it.
The two questions every request has to answer
When a browser asks your app for something, two entirely separate questions have to be answered before the answer goes out.
Who is this? That is authentication. It is solved by a login form, a session cookie, a token. It is the same problem in every application ever written, which is why there are libraries for it, why the libraries are good, and why an AI coding tool gets it right almost every time.
Is this person allowed to have this particular thing? That is authorization. It is different for your application than for anybody else's, because it depends on what your records mean and who is supposed to own them. There is no library for it. It has to be decided, by a person, at every single point where a record can be reached.
The first question has one right answer. The second has as many right answers as your app has endpoints, and every one of them has to be written down.
What the failure actually looks like
Almost always, it looks like this.
Your app shows a user their own invoice at an address ending in /invoices/1043. They are logged in. Everything is correct. Then they change 1043 to 1042 and press enter, and they are looking at somebody else's invoice.
Or the address never changes, because the app is more modern than that, and instead the browser sends a request carrying { "invoiceId": 1043 } in the body. Same thing. Change the number, get somebody else's record.
Or the record comes back correctly but carries a field it should not — a plan tier, an internal note, a flag saying whether the account is an administrator. The row was right; a column on it was none of that user's business.
Or a normal user calls an address that only an administrator was ever meant to call, and nothing stops them, because the only thing hiding the administrator page was that the button was not on their screen.
How you know you have it: you probably do not, yet, because none of the above produces an error. That is the entire problem, and I will come back to it.
The names, because you will meet them
If you hire anyone to look at this, they will use particular words, and it helps to know which is which.
Broken object level authorization is the wrong row — my invoice number gets me your invoice. Older writing, and most security tooling, calls this an insecure direct object reference, usually shortened to IDOR. The two terms describe the same bug. It is the first item on the current OWASP list for APIs, which is the industry's standard ranking of what actually goes wrong.
Broken object property level authorization is the wrong column — I can see the record, but I can also see or change a field on it that was not mine to touch. This is where the classic privilege escalation lives, where somebody sets their own account's role field to administrator.
Broken function level authorization is the wrong endpoint — I can call something that was only ever meant for staff.
All three are variations of one theme: the app checked identity and then forgot to check entitlement.
Why AI tools get this wrong so consistently
This is the part worth understanding properly, because it explains why the problem has not improved even as the tools have got dramatically better at everything else.
An AI coding assistant improves through feedback. It writes something, something happens, and the outcome teaches it whether the code was right. Broken syntax produces an error. A failing login produces a screen you cannot get past. A crashed page produces a stack trace. All of these generate a signal, and the loop closes.
A missing authorization check generates nothing. The page loads. The data arrives. The tests pass, because the tests were written by the same assistant and they test one user at a time. The feature works, in the only sense the tool can measure. There is no signal to learn from, so the loop never closes, and no amount of additional capability fixes it — because it is not a capability problem.
The measurements now say the same thing from several directions.
A study published in July 2026 examined twenty-eight applications built by AI coding tools and verified four hundred and thirty-four distinct exploitable issues in them. Authorization was the second-largest category, at roughly a fifth of everything found. The more interesting number is inside that: authorization accounted for about eleven per cent of the flaws in the small, freshly built applications and about twenty-eight per cent in the large one. The researchers' explanation is the best short account of this problem I have read anywhere — a check that one user owns one record is easy and local, but the same rule at scale spans hundreds of endpoints and depends on both who is asking and what state the record is in.
An earlier assessment, run in December 2025 across five different agentic coding tools with three applications each, found not a single exploitable SQL injection or cross-site scripting flaw in fifteen applications. The serious findings were authorization logic and business logic. The models have thoroughly learned to write parameterised queries. They have not learned ownership, and there is no reason to expect them to, because ownership is a fact about your business rather than a fact about code.
And a widely quoted industry benchmark, updated in July 2026, found that AI-generated code is now syntactically almost perfect while still failing on nearly half of security-relevant tasks — a figure that has barely moved in over a year. It is worth knowing what that benchmark actually tests, though, because people cite it carelessly: it covers injection, weak cryptography, cross-site scripting and log injection. It does not test authorization at all. The most common serious flaw in AI-built applications is not measured by the headline number everybody quotes about AI-built applications.
Row-level security is a wall with five doors in it
If your app is built on Supabase, and most AI-built apps are, you may have been told that row-level security is the answer here. It is a large part of the answer and you should absolutely have it switched on. But it governs direct table access and nothing else, and AI-generated code has a strong tendency to end up on the other side of it. Supabase's own hardening guidance is explicit that policies are one of two layers, not the whole thing.
Here are the five places it does not reach, in rough order of how often I find them.
Database functions. Supabase states it plainly in its own documentation: policies do not apply to functions. A function created the usual way runs with the privileges of whoever created it — normally the project owner, who is exempt from every policy on the system. So a call to a function that fetches an invoice by id runs with the safety rails removed. If that function does not compare the caller's identity against the record's owner inside its own body, it is an authorization hole with a tidy interface on it. This is the single most common one, and there is a specific reason why: when an assistant hits a policy error it cannot resolve — and there is a famous one about recursive policies that half the community has pasted into a chatbot at some point — the fastest way to make the error disappear is to move the query into a function. The error goes. So does the protection.
Views. Same mechanism, less well known. A view built the ordinary way inherits its creator's privileges, so a view over a protected table hands out every row in it. Supabase's own project linter flags this, under a rule about views defined with the creator's security context, and almost nobody reads the linter.
Server code holding the admin key. Every backend has a key that bypasses every policy by design, for the legitimate cases where it must. A serverless function that verifies the caller's token, reads an id out of the request body, and then queries with that admin key has authenticated perfectly and authorized not at all. Verifying a token proves who is calling. It says nothing about what they may have.
Public storage buckets. This one surprises people. On a private bucket, policies govern downloads. On a public bucket they do not — Supabase's documentation is clear that public means anyone holding the address can fetch the file, and that the policies you wrote continue to govern uploading, deleting, moving and copying but not reading. An app that stores scanned documents, identity photos or invoices in a public bucket has no read protection at all, no matter how careful the policies look. And the usual file layout is guessable enough that "the address is secret" is not a defence.
Roles stored where the user can edit them. Supabase provides two metadata fields on every user. One of them can be changed by the user themselves through an ordinary account update. The other cannot. Their documentation says directly that the editable one is not the place to keep authorization data. A policy that decides who is an administrator by reading the editable field is a privilege escalation with a self-service form attached, and it is a natural thing for an assistant to write, because both fields are right there and only one of them is dangerous.
The fix: for each of these the fix is the same in shape and different in detail — the ownership comparison has to happen inside whatever crossed the boundary. Inside the function body. Inside the serverless handler, before the admin key is used. Buckets holding anything personal move to private with signed links. Roles move to the field the user cannot write to.
The clearest example is the platform's own
If you want one case that shows exactly how this failure differs from the ones people usually worry about, it happened to Lovable itself, and to its credit the company published an account of it.
Between early February and 20 April 2026, project source code and chat history that should have been private could be retrieved by other users of the platform. Researchers reported it through the company's disclosure programme starting in late February; the reports were closed without reaching the security team, because the people triaging them were working from internal documentation that described the behaviour as intended. It was fixed within a couple of hours once it was raised publicly.
Look at the shape of it. Nobody bypassed a login. Every person who could reach that data was a legitimate, signed-in account holder, and a free one at that. No database rule was missing. The platform's own interface simply never asked whether the account making the request owned the thing being requested. Authentication was flawless throughout. Authorization was absent.
I will note honestly that the published accounts of this incident do not fully agree — the company's own timeline and a contemporaneous news report differ on the dates the first researcher reports arrived and therefore on how long the exposure ran. I have used the company's own figures above. The disagreement is about the length of the window, not about what happened.
How to test your own app in ten minutes
You need two accounts on your own app and a browser. Nothing else. Only do this on an application you own.
Make the second account real. Sign up properly as a second, unrelated customer. Put a couple of recognisable records in it — an order, a document, a note with a word in it you would spot instantly.
Capture an identifier from the first account. Log in as account A, open something that belongs to A, and look at the address bar. If there is a number or a long code in it, that is your identifier. If the address is uninformative, press F12, open the Network tab, filter to Fetch/XHR, reload, and look at the requests going out — the identifier will be in one of them, either in the address or in the request body.
Now become account B and ask for A's thing. In a different browser or a private window, log in as B, and navigate to the same address with A's identifier in it. If you can see A's record, you have found it. That is the whole test.
Then try the three variations, because each is a separate hole and an app can have one and not the others. Try changing rather than reading — open something of A's from B's session and press save. Try creating a record from B's session while sending A's identifier as the owner. And find any page that only an administrator should see, note its address, and simply visit it as B.
Clean result: every one of those attempts returns nothing, an empty response, or an error. Not a blank page that hides the data with styling — an actual refusal from the server. Check the Network tab rather than the screen, because the difference between "the server refused" and "the server sent it and the page chose not to draw it" is the whole subject of this article.
What fixing it actually involves
I would rather be straight with you about the shape of the work than pretend it is a checkbox.
The first part is quick and genuinely satisfying: find every place a record can be reached and add the ownership comparison. For a small app that is an afternoon.
The second part is the one that takes the time, and it is inventory rather than coding. You cannot protect a path you have not listed. In most AI-built apps I look at, the same table can be reached from a page, an API route, one or two database functions, a scheduled job, an export button and sometimes a webhook that was added months later for something unrelated. Six paths, six independent chances for the check to be missing, and no tool that will tell you the list — you get it by reading.
The third part is deciding what the rules actually are, and this is your job rather than a developer's. Can a team member see records created by a colleague who has since left? Does an administrator of one workspace have any standing in another? When somebody is removed from an organisation, what happens to the records they made? Most AI-built apps have never had these questions asked, which is why the answer defaults to whatever the code happens to do.
If you would rather not do this alone
If you ran the two-account test and found something, or ran it and are not certain what you were looking at, that is a reasonable place to be. Nothing about this was ever explained to you, and the failure is specifically designed by circumstance to be invisible.
I do a Production-Ready Audit for exactly this: every path to every table enumerated, the ownership check verified on each one, functions and views and storage checked separately because they sit outside the policy layer, and a written fix list in priority order rather than a scanner dump. From $499, back in five to seven days. If the test above came back clean I will tell you so and you keep your money.
You can also just send me a screenshot of what you saw. I have looked at a lot of these and I will tell you whether it is serious, at no charge and with nothing attached to it.
The audit and the repair work are described on the AI SaaS rescue page. If you have not yet run the four no-login checks, do those first — they are quicker and they find a different problem, and they are in is my Lovable app secure. The wider list of what tends to be missing at the same time is in the ten problems every AI-built app has in production, and the related question of whether your keys are sitting somewhere public is in are my API keys exposed.
Follow-up questions
What people ask next
My app has row-level security turned on. Am I covered?
Partly, and the gap is bigger than most people expect. Row-level security governs direct table access. It does not apply to database functions, it is bypassed by views created the ordinary way, it is bypassed entirely by any server code using the admin key, and on a public storage bucket it governs uploads but not downloads. Those five paths are exactly where AI-generated code tends to put the logic that failed a policy check.
Is this the same thing as the security checks in your earlier article?
No, and that article says so in its own closing section. Those four checks find data that is readable with no login at all. This is the harder case: everything requires a login, every login works, and the app still hands the wrong person the right data. It needs a second account to find, not a logged-out browser.
How serious is this really if my app is small?
The severity does not scale with your size, it scales with what is in the records. Ten customers whose invoices and phone numbers are readable by each other is a reportable data incident in most of Europe, and the fact that you had ten rather than ten thousand does not change that. The good news is that fixing it at ten customers is a fraction of the work it becomes at ten thousand.
Can I fix this myself?
Sometimes. If the problem is one endpoint missing one ownership check, that is a small, satisfying fix and you should make it. The reason people get stuck is that the check has to be correct in every place a record can be reached, and in most AI-built apps a record can be reached from more places than the person who commissioned it realises: a page, an API route, a database function, a scheduled job and an export button can all touch the same table by different paths.
Related reading
Production-Ready Audit
Every table's row-level security reviewed, keys checked and rotated, auth and payments tested — back as a written fix list in priority order.
From $499 · 5–7 days

Muhammad Bilal
Full Stack AI Developer · Faisalabad, Pakistan
I build and rescue production AI SaaS products with Next.js, Supabase, Stripe and Claude. Most of my work is finishing apps that were started with Lovable, Bolt, Cursor or Replit and stalled somewhere between working and shippable.
5.0★ · 100% job success · 35+ projects delivered
