Is your vibe-coded app secure? The gaps AI coding tools leave

An app built with AI coding tools is usually secure along the paths you tested and open along the ones you did not, because security is mostly code that refuses, and nobody asked for it. The gaps that matter most are secrets shipped to the browser, endpoints that check who is signed in but not what they may touch, paid AI endpoints with no rate limits, and input trusted instead of validated. Each can be found by trying to misuse your own app, and each is fixed on the server, not in the interface.

By Adapted from Chapter 6, API-Driven AI, of From Model to Product ·

Why an app can work perfectly and still be open

A demo is tested by someone using the app as intended. Security is about what happens when someone does not. AI coding tools are good at the first: you describe a feature, they build the path to it, and you click through until it works. What they rarely add unasked is the code that says no: the check that this invoice belongs to this user, the limit on how often an endpoint can be called, the rule that a field sent by the browser is ignored. No happy-path test fails when those checks are missing.

Chapter 6 of From Model to Product, on API-driven AI, treats an AI feature as an API like any other: authentication, authorisation, rate limiting, abuse prevention, input limits and safe failure all apply. Its extra point matters most here: some endpoints consume paid compute, so an open endpoint is a cost problem as well as a security one. For everything else that changes between prototype and product, start with taking a vibe-coded app to production.

Authentication is not authorisation

Authentication answers who is calling. Authorisation answers what that caller may do. The book stresses the difference for AI systems, where it governs cost as well as access: one client may be allowed a cheap classification endpoint but not a costly generative one, or production models but not experimental versions.

In apps built with AI tools, the usual gap is good sign-in followed by blanket trust. The fix in every case below is the same: check permissions on the server, on every request, against an identity the server verified itself rather than one the client claims.

  • The admin page is missing from the menu, but its API route answers anyone with a session. Hiding a button is a design choice, not a permission.
  • The server reads the user ID, role or plan from the request body instead of from the verified session.
  • Every signed-in user can call every endpoint, including the expensive ones.
  • Permission checks run in browser code, where anyone can edit or skip them.

API keys identify an app, not a user

Chapter 6 walks through the common ways a service knows who is calling. The first is the one to understand: an API key identifies an application or consumer, not necessarily a person, and anyone holding it can use it. It is the right tool for your server to call a model provider and the wrong tool for telling your users apart. Match the scheme to the risk: a first product rarely needs more than a sign-in provider for users and server-held keys for everything else.

MethodIdentifiesFits
API key in a headerAn application or integrationYour backend calling a model provider, internal tools, simple partner integrations
Bearer token from an identity providerA user or session, with expiry and claimsSigned-in users of your app
OAuth delegated accessA third-party app acting for a userOther apps calling your platform on a user's behalf
Mutual TLS or network trustA service, at the infrastructure layerInternal calls between your own services

Keep every secret out of the browser

Anything shipped to the browser is public. Chapter 10 of the book makes the case through its worked example: a provider key in the browser can be extracted by anyone who inspects the network traffic or the code, then used at your expense. So the browser never calls a paid provider directly; it calls your backend, which adds the key, makes the request and returns only what the client needs. Chapter 13 adds that secrets should never be hardcoded inside a container image but supplied through the environment at runtime; keep them out of the repository too.

AI-built front ends usually leak keys in one of three ways:

  • A public prefix. Next.js inlines every variable prefixed NEXT_PUBLIC_ into the JavaScript sent to the browser (Next.js docs), and Vite does the same with VITE_. A model provider key with either prefix is published.
  • A provider call written straight into a front-end component, because that was the shortest path to a working demo.
  • A database key with full rights in client code. Supabase's publishable key, the successor to the anon key, is designed for the browser and reaches only what Row Level Security allows. Its secret key, formerly service_role, bypasses every policy and belongs on the server alone.

Check ownership on every object, not just the login

The flaw OWASP ranks first among API risks is Broken Object Level Authorization. An endpoint takes an ID, such as /api/invoices/123, and returns the record without asking whether it belongs to the caller. Change 123 to 124 and you are reading a stranger's invoice. AI tools produce this easily, because the prompt that asked for an invoice page never mentioned other people's invoices.

Fix it on the server by fetching every record through the caller's verified identity, as below, so a missing record and someone else's record get the same answer. If your data lives in Supabase, add a second layer in the database: enable Row Level Security on every table in an exposed schema and write policies that compare each row's owner with the signed-in user. Supabase's documentation warns that a table in an exposed schema without RLS is readable and writable by any role with a grant on it.

type Invoice = { id: string; ownerId: string; total: number };

class NotFoundError extends Error {}

const invoices = new Map<string, Invoice>([
  ["inv_1", { id: "inv_1", ownerId: "user_a", total: 120 }],
  ["inv_2", { id: "inv_2", ownerId: "user_b", total: 80 }],
]);

// userId comes from the verified session, never from the URL or the body.
export function getInvoice(userId: string, invoiceId: string): Invoice {
  const invoice = invoices.get(invoiceId);
  // One answer for "missing" and "not yours", so IDs cannot be probed.
  if (!invoice || invoice.ownerId !== userId) {
    throw new NotFoundError("Invoice not found");
  }
  return invoice;
}

console.log(getInvoice("user_a", "inv_1").total); // 120

try {
  getInvoice("user_a", "inv_2"); // user_b's invoice
} catch (err) {
  console.log(err instanceof NotFoundError); // true
}
An ownership check in plain TypeScript. In a real app the rule goes into the query itself: select the record by its ID and its owner together.

Rate limits and budgets on AI endpoints

An AI endpoint is where an attacker, a bug or an enthusiastic user turns straight into a bill. Chapter 6 treats rate limiting as a financial and operational concern as well as a security one: a cap on requests per client per window protects the service from abuse, accidental overload and runaway cost. OWASP lists the same risk as Unrestricted Resource Consumption.

Backpressure is the other half: deciding in advance what happens when demand exceeds what you can afford. The chapter's options are to refuse politely, queue the work, switch to a smaller model or switch off non-essential features. For an app built with AI tools, that becomes a short list:

  • Require sign-in for every endpoint that calls a paid model, so each call has an owner to count against.
  • Limit requests per user and per IP address, with a lower limit for anonymous traffic. Keep counters in a shared store; an in-memory counter resets on restart and is not shared between instances.
  • Give each user a daily budget of model calls or tokens, enforced on the server before the provider is called. LLM cost per user shows how to size it.
  • Cap input length and output tokens, so one request cannot carry a novel-length prompt.
  • Authorise by cost: if only paying users may reach the larger model, check the plan on the server.
  • Set a spending limit or budget alert in your provider's console where one is offered, as the last line of defence.

Validate every input against a strict schema

Predictability matters more than cleverness in APIs, the chapter argues. Loose schemas make clients harder to build and let bad input travel deep into the system before anything breaks. The API boundary is the place to reject it, before it wastes model time.

A strict schema closes three gaps at once. It rejects malformed data. It caps sizes, including the length of text sent to a model. And it drops fields the client should not set: if a profile update accepts whatever JSON arrives, a user can add a role or plan field and promote themselves, which OWASP files under Broken Object Property Level Authorization. In TypeScript, Zod strips unknown keys by default and rejects them with z.strictObject; in Python, Pydantic rejects them when extra fields are forbidden, as in the FastAPI deployment guide. Either way, save the parsed result, never the raw body.

Safe failure belongs here too. When something breaks, send the client a short generic error and keep the detail in your logs; raw exception text tells an attacker how the system is built.

A security check you can run on your own app

Turn the chapter's list of concerns into tests against your deployed app, signed in as two different test users. None of them needs a security tool.

  1. Secrets: load the built app and search its scripts and network calls for the first characters of each key. Nothing secret should appear.
  2. Authentication: call each API route without signing in. Only public routes should answer.
  3. Authorisation: as user A, request user B's records by changing the ID in the URL or the body. Every request should fail.
  4. Roles: as an ordinary user, call the admin routes directly and send a role or plan field in an update. Both should be refused.
  5. Rate limits: call the AI endpoint in a loop. It should start refusing well before the bill matters.
  6. Input: send an oversized prompt, a wrong type and an unknown field. Each should be rejected before any model call.
  7. Failure: force an error and read the response. It should say something went wrong and nothing about your stack.

AI APIs are still APIs. That means all the usual concerns remain: authentication, authorization, rate limiting, abuse prevention, input limits, and safe failure behaviour. An inference service may be computationally expensive, which makes rate limiting especially important. Unbounded access can create both security and cost problems.

From Chapter 6 of From Model to Product

Questions

Is code written by AI coding tools secure by default?

Not reliably. The tools write what the prompt and the surrounding code ask for, so they often produce sound code for the paths you described and leave out checks nobody mentioned, such as ownership, rate limits and field allowlists. Read the parts that touch identity, data and money as you would a new colleague's first pull request, and test the app by trying to misuse it.

Is it safe to put the Supabase anon key in my frontend?

Yes, provided Row Level Security is enabled on every table it can reach and the policies are right. Supabase designs the publishable key, the newer form of the anon key, for browsers and apps, and RLS decides what it can read and write; with RLS on and no policies, it reaches nothing. The secret key, formerly service_role, bypasses every policy, so it must never appear in client code, a shipped app or source control.

What should I do if an API key was committed to my Git repository?

Revoke or rotate the key at the provider first, then remove it from the code and move it into the host's environment settings. Deleting the line is not enough, because the old value stays in the repository history and in every clone. Then check the provider's usage for the period it was exposed.

What is an IDOR vulnerability?

IDOR, short for insecure direct object reference, is a flaw where changing an identifier in a URL or parameter lets someone read or modify a record that is not theirs. The cause is a missing object level authorisation check, the risk OWASP's API Security Top 10 ranks first as Broken Object Level Authorization. The fix is an ownership check on the server for every object access, backed by a database policy where you can add one.

How do I stop users running up my AI model bill?

Route every paid model call through your own backend and behind sign-in, then limit it in layers: requests per user per minute, a daily call or token budget per user, caps on input and output length, and a spending limit or alert at the provider. Decide what each limit does, such as a smaller model, a queue or a polite refusal, so real users get a clear answer rather than an error.

Can I ask the AI tool to review its own code for security?

Yes, as a first pass. Ask it to list every route, who may call it and what it checks, then compare that with what the code actually does. A review by the same tool can share the assumptions it made while writing, so back it with the behavioural tests in this guide, and if the app takes payments or holds health, financial or children's data, add a review by an experienced engineer.

About Dr. Dulan Dias