Security findings are the smallest category in most of our scans and the only one that can end a company. They are also the most predictable. Ask an assistant to build a feature and the security gaps arrive in the same five shapes almost every time, because a prompt ends exactly where security begins: the code works, and nobody asked what happens when the caller is hostile.
Here are the five, with how the assistant produces each one and the fix that closes it.
1. The key in the constant
STRIPE_KEY = "sk_live_51H..."
A key pasted from a dashboard so the demo runs, then committed with everything else. The assistant has no idea what your secret store is, so it keeps the constant where it found it and reuses it in the next file. A live key in a repository is the single most expensive finding in this list, because the repository history keeps it even after you delete the line.
Move the value to environment variables or encrypted credentials, add the environment file to .gitignore, and rotate the key whether or not you think anyone saw it. The full order of operations is in what to do when a key is already committed.
2. The endpoint that never asks who you are
export async function GET() {
const orders = await db.order.findMany()
return Response.json(orders)
}
The handler does the work and skips the question. In Rails it is a controller with no before_action to authenticate; in FastAPI a route with no auth dependency; in Next.js a route handler like the one above. The assistant wrote what the prompt described, and the prompt described the data, not the caller.
The fix is a session or token check on the first line, returning 401 before any work happens, and then an ownership check so a signed-in user only sees their own rows. Finding every unprotected Next.js route walks through the audit.
3. SQL assembled from strings
Order.where("user_id = #{params[:user_id]}")
In Python it is an f-string passed to cursor.execute; in JavaScript a template literal passed to db.query. Any value that reaches that line from a request can rewrite the query. Assistants produce this because interpolation is the shortest path from "filter by user" to working code.
Order.where(user_id: params[:user_id])
Bind the value instead of splicing it. Every driver and ORM supports placeholders, and the change is usually a single line per query.
4. Mass assignment
User.create(params[:user])
Or in Laravel, User::create($request->all()). The request body is handed straight to the model, so a caller can set any column the model has: an admin flag, a foreign key, a price. It is the natural output of "create a user from the form", and it is invisible until someone reads the model's columns and gets ideas.
Whitelist the attributes: strong parameters in Rails, $fillable plus validated() in Laravel, an explicit schema in anything else. Pass only the fields the form is allowed to set.
5. The open redirect
redirect_to params[:return_to]
The sign-in flow needs to send people back where they came from, so the assistant reads the destination from the request and redirects to it. Now a phishing link can send a signed-in user through your domain to an attacker's page, with your name in the address bar for the first hop.
Only redirect to a path you validated: compare against an allowlist of routes, or strip the host and keep the path. Most frameworks have a helper for exactly this, such as url_from in Rails or a relative-path check before res.redirect.
Why assistants make these
Not carelessness. The model optimizes for the prompt it received, and the prompt said "make it work". Every one of the five is the shortest correct answer to a feature request, and every one of them becomes wrong the moment you add a hostile caller to the picture. The second question, "and what if the caller is lying", is never in the prompt unless you put it there.
Keep them out
- Run a secret scanner and a rule-based scanner in CI, so the next occurrence never merges.
- Put authentication in one place every private route inherits, and list the exceptions by name.
- Add "handle a hostile caller" to your prompt template for anything that reads a request.
Rotwise treats all five as P0 or P1 findings, confirms each one against the surrounding files before it counts, and puts them in the first batch. Whatever tool you use, fix these five before anything else in the report.