Access control for AI-built apps

We try the doors your builder forgot to lock.

Lovable, Bolt, v0 and Firebase Studio write the screens. The part they get wrong is the rule deciding which user may read which record. DenyFirst asks your backend for data from an account that owns nothing, then shows you what came back.

// if nothing opens, you pay nothing
// read-only by default · only ever on apps you prove you own

The bug we prove

One character. Somebody else's invoice.

Jae asks for their own invoice. The app sits behind a login, the request is signed in, and everything looks exactly right.

  1. 01A signed-in user asks for their own record. 200 OK. Nothing looks wrong.
  2. 02Change one character in the URL. 184 becomes 185. Same session, different owner.
  3. 03The backend answers anyway. It returned a record belonging to someone else, because no rule ever checked who was asking.
  4. 04You get the rule that closes it. Plus the request that produced the leak, so you can verify it yourself.
  5. 05Re-scan. The same request now comes back denied.
live probe session: jae@acme.io

GET /api/invoices/184

response · 200 OK

id184
ownerjae@acme.io
total$2,400.00
cross-tenant read · confirmed
the rule that shuts it allow read: if request.auth != null;
allow read: if request.auth.uid == resource.data.ownerId;
403

What comes back

Six ways a backend answers a stranger.

Not a checklist of things we look at. These are the findings themselves, in the shape you receive them: what we asked, what came back, and the line that ends it.

Finding 01 · cross-tenant read

Your records answer to anyone

The classic. Swap an ID in the URL and the database hands over a row belonging to a different customer.

GET /rest/v1/orders?id=eq.4471
as: anonymous
200 · 1 row · owner: another tenant

The fix we hand youUSING (auth.uid() = user_id)

Finding 02 · open write

A stranger can edit your data

Worse than reading. We check whether an account that owns nothing can create, overwrite or delete. The check runs inside a transaction the database is asked to roll back.

PATCH /documents/inv_88
as: anonymous
200 · write accepted · rolled back

The fix we hand youallow write: if request.auth.uid == resource.data.ownerId;

Finding 03 · no rules at all

A table nobody wrote a policy for

Row-level security never switched on, or a Firestore collection with no matching rule. Not a subtle mistake. The boundary was never created.

table: public.profiles
rls: disabled
every row readable by the anon key

The fix we hand youALTER TABLE profiles ENABLE ROW LEVEL SECURITY;

Finding 04 · exposed bucket

Uploads anyone can list

Avatars, invoices, ID scans. Buckets are the part a builder rarely gets to, and a listable one gives away every filename inside it.

GET /storage/v1/object/list/avatars
as: anonymous
200 · 1,284 objects listed

The fix we hand youpublic = false, plus a per-owner storage policy

Finding 05 · leaked key

The admin key is in your bundle

A service-role key shipped into the JavaScript every visitor downloads. It bypasses every rule you have, so nothing else on this list matters until it's rotated.

/assets/index-4f2c.js
match: service_role JWT
bypasses RLS entirely

The fix we hand yourotate it, move it server-side, ship the anon key only

Finding 06 · auth misconfig

Anyone can mint an account

Anonymous sign-in left switched on, or open self-registration. Every rule that trusts "is signed in" now trusts the whole internet.

POST /auth/v1/signup
email confirmation: off
session issued instantly

The fix we hand yougate on ownership, not on the existence of a session

Every finding ships with the request that produced it. Verify it yourself, fix it, re-scan.

Why this isn't a scanner

A scanner gives you 8,000 maybes.

Our test account owns nothing, so if it reads a record, that record is somebody else's. There is no severity argument to have. And a catalogue that is public on purpose gets labelled public, not flagged as a breach.

  • WARN · CVE-2019-11358 · jquery@3.3.1 · severity UNKNOWN · reachable? unknown
  • INFO · missing header X-Frame-Options · confidence LOW
  • WARN · CVE-2021-23337 · lodash · transitive · no known exploit path
  • INFO · cookie without SameSite · 14 occurrences
  • WARN · TLS 1.1 advertised on legacy edge · informational
  • INFO · directory listing possible at /static · confidence LOW
  • WARN · CVE-2020-28168 · axios · version range overlap · unverified
  • INFO · verbose server banner disclosed
  • WARN · outdated dependency tree · 412 advisories · unprioritised
  • INFO · missing Content-Security-Policy · confidence MEDIUM
  • WARN · CVE-2022-25883 · semver · dev dependency · never shipped
  • INFO · mixed content on 3 assets
  • One finding. An account that owns nothing read invoice #185. Here is the rule that stops it.

Before a single packet

Try to point us at an app you don't own.

A tool that probes backends is only as trustworthy as the thing that stops it. So the first thing DenyFirst does is refuse. No ownership proof, no packets. That isn't a policy we describe, it's one you can test right here.

https://someone-elses-app.vercel.app
> resolving target… > checking authorization receipts… > 403 · no ownership proof on file > 0 packets sent. nothing was touched.

What we keep

The scanner deletes itself.

We need enough access to test your boundaries. We don't need permanent access to become one more thing that has it.

01

Prove it's yours

OAuth into the Firebase or Supabase project. The authorization receipt is signed and downloadable by both sides.

02

A box made to be thrown away

An isolated sandbox reads your public client config. Nothing ever runs on your infrastructure.

03

Read-only by default

Write checks are opt-in, run inside a transaction the database is asked to roll back, and are verified gone after.

04

Findings, never contents

We record that a record was reachable, and its field names. Your users' data is never stored.

05

Access expires

The box is destroyed and the grant lapses. Anything that could write expires within the hour, and you can revoke sooner.

Pricing

You're paying for a way in, not a green dashboard.

Every scan is a live probe against a backend you've proven you own, so there's no free tier to give away. Here's what stands in for one: if we can't get in, you don't pay for the attempt.

Pro · where most teams start

$199/mo, billed annually

$2,388 a year · or $299 month-to-month with no lock-in

  • 3 apps, unlimited on-demand scans
  • All six probe classes, run against your live backend
  • Every finding with its request and the exact rule to paste
  • Re-scan to prove the fix landed
Start the first scan
The guarantee

If nothing opens, you pay nothing.

A clean scan means a full refund, claimed from your dashboard in one click. The moment a scan does surface a way in, you're holding the exposed endpoints and the fix. That's the audit, delivered, and it stands. Exact terms on the legal page.

Agency

Scanning client apps? The same proof across 20 projects. $499/mo annually.

Compare

Enterprise

Unlimited apps, SSO, audit logs, CI/CD and API access, an SLA.

Talk to us

Annual billing is the best rate. Month-to-month cancels anytime.  Full pricing and FAQ →

Let us try the doors.

Twenty minutes from OAuth to a report naming the collection, the rule, and the request that got through.

See if we can get in

// read-only by default · proof before packets
// findings only, never your users' data