Guide — reading a clean security scan

Your AI-built app passed the security scan. That is not the same as being secure

A clean scan is genuinely good news — and it answers a much narrower question than the one you are asking. Here is what a scanner can and cannot see, the specific classes of defect that sail through a green result, and the checks you can run yourself in an afternoon with two test accounts and a browser. Written so it is useful even if you never hire anyone, including me.

11 min read Updated 27 August 2026 By Appaya Ltd

The short version

What a scanner sees — and what it cannot

An automated scanner, whatever badge it carries, works one way: it compares your app against a library of known-bad signatures. A dependency version with a published vulnerability. A missing security header. A protection that is switched off entirely. A string in the code matching the format of a known secret key. Every one of these is a presence-or-absence question with a universal right answer, and pattern-matching answers them quickly, cheaply and repeatedly. That is real value, and I am not going to pretend otherwise — run the scanner.

Now consider what is not on that list. "Only a project's members may read its documents." "Only staff may change an order's status." "A user may edit their own profile and nobody else's." These are the rules that make your app yours, and they exist nowhere a scanner can look — not in a signature database, not in the code as a checkable assertion, only in your head and your terms of service. A scanner cannot verify that an access rule matches your intent, because it does not know your intent.

Which produces the asymmetry this whole page turns on: a missing control fails the scan, but a wrong control passes it. A database policy that reads, in effect, "allow everyone" is present, enabled and syntactically valid. Green tick. It also hands every row to anyone who asks.

Why AI-built apps pass anyway

Code generators are trained on millions of applications, so they are remarkably good at producing the outward form of security: every route gets an authentication middleware, every table gets a policy, every config gets its headers. The form is what a scanner checks, the form is all there, and the score is high. Whether the middleware actually restricts anything, and whether the policy separates the roles you need separated, is the part generators get wrong most often — and the part no scanner inspects.

This is not a hypothetical failure mode. In 2025 a researcher disclosed CVE-2025-48757: Lovable-generated apps shipping with Row Level Security missing or wrong, with a confirmed count of 170+ live applications whose email addresses, messages, payment records and API keys were readable by anyone holding the public anon key (Superblocks, TNW). The scanner the platform shipped in response checks whether RLS is enabled, not whether a policy is correct — a policy of USING (true) is enabled, passes, and returns every row in the table. Nor is this one platform's carelessness: Veracode's 2025 study across 100+ models found AI-generated code chose the insecure implementation about 45% of the time, with no improvement in newer models (Veracode).

So a generated app and a scanner are a specifically bad pairing: the generator's most reliable output is exactly the thing the scanner measures, and the generator's least reliable output is exactly the thing the scanner cannot see.

The defect classes that survive a clean scan

1 — Authorisation that trusts client-supplied identifiers

The request says "fetch record 4821" and the server fetches record 4821, checking only that the requester is signed in — never that record 4821 belongs to them. Change the number, read someone else's record. OWASP calls this broken object level authorization and ranks it the number-one API security risk (OWASP API Security Top 10). To a scanner the endpoint looks fine: authentication is present. Whose record comes back is invisible to it.

2 — Permissive row-level security

The database enforces per-row access through policies (Supabase docs), and generators dutifully create one per table. The failure is a policy scoped to "any signed-in user" where it should say "the row's owner" — or the blunt USING (true). Supabase's own database advisors catch the crude cases, RLS disabled or enabled with no policy at all; they do not judge whether the policy that exists is right, and nothing automated currently does.

3 — Secrets reachable from the browser

Anything compiled into your frontend bundle is delivered to every visitor — that is what a frontend is. Publishable keys are designed for this; admin, service-role and secret keys are not, and a generator that needed a privileged call to work will sometimes put the privileged key where the call was: in client code. Repository scanners look for known key formats in the code they are shown; a key pasted into a hosting dashboard's environment settings and injected at build time can reach the bundle without ever appearing in the repository at all.

4 — Endpoints that authenticate but never authorise

Authentication asks "who are you"; authorisation asks "and are you allowed". They are separate steps, and generated code regularly ships the first without the second — an admin endpoint any signed-in user can call, an update route that accepts changes to fields the user should never touch. Broken access control tops OWASP's list of web application risks (OWASP Top 10, A01), and it is precisely the class a scanner cannot detect: the check that is missing was never a pattern in the first place.

5 — Webhooks that believe anyone

A webhook is an endpoint that accepts instructions from the outside world — "this payment succeeded", "this subscription was cancelled" — and acts on them. Providers sign their deliveries so your handler can verify the message really came from them; both Stripe and GitHub document signature verification as required practice. A handler that skips the verification and trusts the payload will act on anything sent to its URL. From the scanner's side there is nothing to see: the endpoint exists, responds, and looks like every other endpoint.

6 — APIs that return more than the page shows

The page displays a name and an avatar; the API response behind it carries the full database row — email address, phone number, internal flags, sometimes tokens. Everything in that response is available to anyone who opens their browser's developer tools, whatever the page chooses to render. OWASP tracks this as broken object property level authorization (OWASP API Security Top 10). Generators produce it constantly, because returning the whole object is the path of least resistance — and no scanner knows which fields your page meant to show.

The checks you can run yourself

Everything below runs against your own app, needs no tools beyond a browser and two test accounts you create yourself, and takes about five minutes per check. This is the part of the page I would ask you to keep even if you close the tab afterwards.

  1. The two-account test. Create two accounts of your own. Signed in as the first, open a record only that account should see, and note its identifier in the address bar or the network tab. Sign in as the second account in a private window and request that same identifier. Pass: refused, or not found. Fail: the second account reads the first account's data. Repeat for the most sensitive record type you hold.
  2. The signed-out test. Copy the address of a page showing private data, sign out, and open it in a private window with the network tab open. Pass: you land on the login screen and no data arrived in any response along the way. Fail: the page redirects but a response in the network tab quietly contained the data anyway.
  3. The response-body test. Open developer tools, load a page that lists other users or shared content, and read the actual API response. Compare it to what the page displays. Fail: email addresses, phone numbers, tokens or internal fields in the response that the page never shows.
  4. The bundle test. In developer tools, search the JavaScript your app delivers for your providers' secret-key prefixes — sk_live, service_role — and for the word secret. Anything found in a file the browser downloaded is public, whoever it was meant for.
  5. The history test. Run a history-wide secret scanner such as gitleaks or trufflehog over your whole repository, not the latest commit. A key committed early and "removed" later is still in the history, and still live until rotated.
  6. The webhook question. For each webhook handler in your code, check that the first thing it does is verify the provider's signature — the Stripe and GitHub pages linked above show exactly what that looks like. If you cannot tell whether it does, that is itself the finding to hand a developer.

A failure on any of these is not a maybe — it is a real defect, and each one has a specific, bounded fix. A clean sweep is meaningful too: it says the most common classes of generated-app failure are absent from the paths you tested.

What a review adds beyond the scan

Keep the scanner. It is the cheap, continuous layer, and it catches the crude cases the moment they appear — which a human review cannot do economically. What a review adds is the layer the scanner is structurally blind to: a person reads each access rule against the roles it is meant to separate, traces the two or three riskiest flows end to end — sign-up and role assignment, anything touching money or other people's data — and checks that what the code enforces is what the business means. The checklist above samples that work; a review does it exhaustively, policy by policy, and writes down what it checked so you can see what was and was not covered.

If you want help

You may not need any. If the six checks pass, your app holds nothing especially sensitive, and you rerun them after each significant generated change, you are ahead of most teams at this stage and can reasonably stop there. Where I would want a review is when a check fails and you are not sure how far the problem extends, or when you hold other people's data and want the access rules actually read rather than sampled. That is what Appaya's review is: the work above, done exhaustively against your real repository, with a report that says plainly what was checked — and says so if everything is fine where it is.

Request a fit check How the audit works