Guide
How to review the security of an app you built with AI
9 minute read · Published
Code written with an assistant is not worse than code written by hand. It is written faster, and the review that used to happen in the gaps — while you were reading documentation, while you were waiting for a colleague — does not happen at all. The mistakes below are not evidence that the tool is careless. They are evidence that nobody asked it the second question.
Each section names a pattern, explains why it recurs, and gives you something concrete to check. You do not need to be a security engineer to work through it; you need an hour and your editor’s search.
1. Authorization that was never asked for
This is the most common serious finding, by a distance. You asked for an endpoint that returns an order. You got one:
export async function GET(request, { params }) {
const order = await db.order.findUnique({ where: { id: params.id } });
return Response.json(order);
}It works. It returns the order. Nothing about the code looks wrong, because nothing about it is wrong with respect to what you asked. What is missing is the check that the person making the request is allowed to see that particular order — and you did not ask for that, because when you are describing a feature you describe the happy path.
What to check. Search your codebase for every place a record is looked up by an identifier that came from the request: a path parameter, a query string, a field in the body. For each one, answer out loud: what stops somebody putting a different id here? If the answer is “the user interface only shows them their own”, you have found one. The interface is not a control; anybody can call the endpoint directly.
What to do. Put the owner in the query rather than checking it afterwards. A lookup constrained to { id, userId: session.userId } cannot return somebody else’s row, whereas a lookup followed by an if can be skipped by the next person who copies the pattern and forgets the second half.
2. Secrets that ended up in the repository
An assistant will happily write const stripe = new Stripe("sk_live_…") if that is what makes the example run, and a file created before you set up .gitignore is committed the first time you run git add .. The second case is the one that catches experienced developers too.
What to check. Search for the prefixes your providers use — sk_, rk_, AKIA, ghp_, -----BEGIN — and then check your history, not just your working tree: git log --all -p -S "sk_live". A key you deleted in a later commit is still in the repository, and anybody who can clone it can read it.
What to do. Rotate it. Removing a secret from a repository does not un-disclose it, and rewriting history does not help if the repository was ever public or ever cloned. Rotate first, clean up second.
3. Payment flows that trust the browser
Payment integrations are long, and the parts that get shortened are the parts that are not required to make a test transaction succeed. Two failures recur.
The first is a webhook handler that reads the event body without verifying the signature. It works perfectly in testing, because the only thing sending events is your provider. It also means anybody who knows the URL can tell your application that an invoice was paid.
The second is an amount that arrives from the client. If the browser sends { priceId, amount } and the server charges amount, the price of everything you sell is a number in somebody’s developer tools.
What to check. Find your webhook route and confirm it verifies a signature before it does anything with the payload — and that it rejects, rather than logs and continues, when verification fails. Then find where you create a charge or a checkout session and confirm the amount comes from your own database, keyed by a product identifier, never from the request.
Also check. That a duplicate event is harmless. Providers retry, so a handler that grants a month of access every time it is called will eventually grant several.
4. Dependencies nobody has looked at since
The packages were current on the day they were installed. That day was some months ago, and advisories are published continuously. This is not an AI problem at all; it is on this list because a project built in a weekend is a project with no maintenance habit attached to it yet.
What to check. Run your package manager’s audit command. Read the output rather than the summary count: what matters is whether the vulnerable code path is one your application actually uses, and a build-time-only dependency with a high-severity advisory is usually less urgent than a request-handling one with a medium.
What to do. Update, then run your tests. If an update is a major version you cannot take today, write down why and when you will — an accepted risk with a date is a decision, and an accepted risk without one is something you forgot.
5. Configuration that was fine on your laptop
Development settings that survive to production are their own category: a permissive CORS rule added to make a local frontend work, a debug endpoint that returns environment variables, a session cookie without the Secure or HttpOnly attribute, an error page that shows a stack trace.
What to check. Load your production site and look at the response headers. Does the session cookie have HttpOnly, Secure and a SameSite value? Does an unhandled error show a stack trace? Try requesting a route you believe is internal, signed out, and see what comes back. Search your configuration for "*" in anything CORS-related and for any comparison against NODE_ENV that turns a check off.
A rough order to work in
- Secrets in the repository and its history. Rotate anything you find.
- Authorization on every endpoint that reads or writes a record by id.
- Payments: webhook signature verification, then amounts and entitlements resolved on the server.
- Dependency advisories, with a decision recorded for anything you do not fix.
- Production configuration: headers, cookies, CORS, error output, debug routes.
The order is by how bad the outcome is if you skip it, not by how much work it is. A disclosed payment key and a customer reading another customer’s data are the two things you cannot walk back with an apology.
What a scan adds to doing this by hand
Working through the list yourself is worth the hour, and you will find things. What you will not do reliably is repeat it: after the third feature, the checklist is in a document nobody has opened. Automation is worth having for the repetition rather than for the cleverness.
Audixa runs these checks against your repository and, once you have proved you control the domain, against the configuration your site serves. Every result comes with the code it matched, an explanation of what could happen, and the steps to fix it — and when you have fixed something, the next scan tells you whether it is actually gone. See what it checks, or read what happens during a scan.
One thing it will not tell you: that your application is secure. A scan with no findings means the checks that ran did not find anything, and knowing which checks ran is the difference between that being useful information and being reassurance.
Run the checks in this guide automatically
One project and one scan a month are free. You see how many findings there are and how severe they are; titles and evidence unlock on a paid plan.