cd /news/ai-tools/the-bug-every-ai-coding-tool-ships-a… · home topics ai-tools article
[ARTICLE · art-127804] src=dev.to ↗ pub= topic=ai-tools verified=true sentiment=· neutral

The bug every AI coding tool ships, and how to prove it is gone

A developer released AuditAI Scanner, an open-source tool that detects a common authorization flaw in AI-generated code where service-role keys bypass row-level security, letting one tenant read another's data. The scanner reproduces each finding in an offline sandbox with synthetic tenants, promoting issues from candidate to confirmed only when a request like Alice reading Bob's invoice returns 200 before a fix and 403 after. It reports a coverage line instead of a score and runs locally via npx auditai-scan.

by read3 min views1 publishedSep 12, 2026

The scanner is open source and the rules are readable before you trust a single finding: github.com/audit0/auditai-scanner. This post is about the one bug it was built for.

Ask any AI coding tool for an invoicing app and you will get something close to this route handler.

// app/api/invoices/[id]/route.ts
import { createClient } from "@supabase/supabase-js";const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_ROLE_KEY!);

export async function GET(_req: Request, { params }: { params: { id: string } }) {
  const { data } = await supabase.from("invoices").select("*").eq("id", params.id).single();
  return Response.json(data);
}

It works. It passes review if the reviewer is reading for "does this return an invoice". The service-role key bypasses row level security, and the query filters on id alone, so Alice can read Bob's invoice by changing a number in the URL.

This is not an exotic bug. It is the default outcome of asking for a feature and not asking who is allowed to see it, and it shows up over and over in apps built with Lovable, Bolt, v0, Cursor and Claude Code.

A scanner that prints "possible authorization issue" at this line has told you almost nothing. You still have to open the file, work out whether the route is reachable, whether some middleware already blocks it, and whether the id is scoped somewhere you did not read. Most teams do that work once, find two false positives, and stop reading the tool's output.

"The model says it fixed it" is worse. A model that both writes the fix and grades the fix is not evidence of anything.

So we picked a harder bar. A finding stays likely until something outside the model reproduces it.

We copy the app into a sandbox with no internet access, seed two synthetic tenants, and send the same request twice.

proof · AUDIT-001
GET /api/invoices/42 as Alice
✗ before fix: 200 OK
✓ after fix:  403 Forbidden

Alice is not supposed to see invoice 42. Before the fix the endpoint hands it over. That is the evidence. It is also, conveniently, a test.

The fix is the smallest one that closes the path, not a refactor:

-const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_ROLE_KEY!);
+import { createServerClient } from "@/lib/supabase/server"; export async function GET(_req: Request, { params }: { params: { id: string } }) {
-  const { data } = await supabase.from("invoices").select("*").eq("id", params.id).single();
+  const supabase = await createServerClient();
+  const { data, error } = await supabase.from("invoices").select("*").eq("id", params.id).single();
+  if (error || !data) return new Response("Forbidden", { status: 403 });
   return Response.json(data);
 }

The request now runs as the signed-in user, so the table's row level security policy decides the answer instead of the route.

The regression test is the reproduction, kept:

it("does not let one tenant read another tenant's invoice", async () => {
  const res = await fetch(`/api/invoices/${bobInvoiceId}`, { headers: aliceAuth });
  expect(res.status).toBe(403);
});

It failed before the fix. It passes after. If someone reintroduces the service-role client next quarter, it fails again.

Every result ends with a coverage line rather than a score:

checked 12 routes · verified 2 · confirmed 1 · unverified 0

unverified is not a failure to hide. It is the set of findings we could not reproduce, and we would rather show you the number than dress it up. Findings move from candidate to likely to confirmed only on evidence, and a fix is verified only when the regression test failed before it and passes after it, the existing suite still passes, and a deterministic rescan no longer sees the path.

We measure precision on repositories we have never seen, and publish the number instead of claiming one.

npx auditai-scan .

It runs locally, needs no account, and sends nothing anywhere. Source, rules and the eval fixtures are here: github.com/audit0/auditai-scanner. The hosted product that reproduces, fixes and proves is at auditai.sh.

If you have a Next.js + Supabase app and want us to find and prove one real authorization bug in it for free, ten design-partner slots are open.

── more in #ai-tools 4 stories · sorted by recency
── more on @auditai scanner 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/the-bug-every-ai-cod…] indexed:0 read:3min 2026-09-12 ·