Why Security Is the First Thing That Gets Skipped
When you are building fast with AI, the goal is momentum. You prompt, it generates, you ship. Security checks slow that loop down, they are invisible when they are working, and they feel like someone else's problem until the moment they are not. The result is that most vibe-coded repos land in production with the same handful of critical issues baked in.
This checklist covers the ten issues that appear most often. None of them require rewriting your app. Each one is a targeted fix that closes a real attack surface before a real attacker finds it.
The 10 Checks
1. Exposed API Keys and Secrets in Code
Bots scrape GitHub for exposed credentials continuously. A live Stripe key or OpenAI key committed to a public repo will be found and used within minutes. Even in private repos, secrets in code become a liability the moment a contractor joins, a repo setting changes, or a breach leaks your git history.
❌ secret hardcoded in sourceconst stripe = new Stripe('sk_live_4eC39HqLyjWDarjtT7en');
const openai = new OpenAI({ apiKey: 'sk-proj-abc123...' });✅ secret loaded from environmentconst stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });Move every secret to an environment variable, add a .env.example with placeholder values, and confirm your .gitignore covers .env*. Rotate any key that has ever touched a commit, even once.
2. Missing Authentication on API Routes
AI-generated API routes often implement the happy path without checking who is making the request. If your route reads or writes user data, it needs to verify identity before doing anything else. An unprotected route is not just a bug; it is an open door.
❌ no auth check on a sensitive route// anyone can call this and get any user's data
export async function GET(req: Request) {
const { userId } = await req.json();
return Response.json(await db.user.findUnique({ where: { id: userId } }));
}✅ session validated before data is returnedexport async function GET(req: Request) {
const session = await getServerSession(authOptions);
if (!session) return new Response('Unauthorized', { status: 401 });
const user = await db.user.findUnique({ where: { id: session.user.id } });
return Response.json(user);
}Audit every route in your app/api directory. Any route that touches a database or performs an action on behalf of a user must verify a valid session first.
3. No Input Validation or Sanitization
Unvalidated input is the root cause of most web vulnerabilities. If you accept a string, a number, or a file from a user and pass it directly into your logic, you are trusting them not to send something unexpected. That trust is always misplaced eventually.
❌ raw user input passed straight to the databaseexport async function POST(req: Request) {
const body = await req.json();
await db.post.create({ data: body }); // body could contain anything
}✅ input parsed and validated with a schemaimport { z } from 'zod';
const schema = z.object({
title: z.string().min(1).max(200),
content: z.string().min(1).max(10000),
});
export async function POST(req: Request) {
const parsed = schema.safeParse(await req.json());
if (!parsed.success) return new Response('Bad Request', { status: 400 });
await db.post.create({ data: parsed.data });
}Use a schema library like zod or yup to define exactly what shape you expect. Reject anything that does not match before it touches your database or business logic.
4. SQL and NoSQL Injection Vulnerabilities
Injection is one of the oldest vulnerabilities on the web and still one of the most common in AI-generated code. When you interpolate user input directly into a query string, an attacker can escape your query and run their own. The fix is always to use parameterized queries or a query builder that handles escaping for you.
❌ user input interpolated into raw SQLconst results = await db.query(
`SELECT * FROM orders WHERE user_id = '${req.query.userId}'`
);✅ parameterized query keeps input and code separateconst results = await db.query(
'SELECT * FROM orders WHERE user_id = $1',
[req.query.userId]
);If you are using an ORM like Prisma, you get this protection by default as long as you do not reach for queryRaw with interpolated strings. Search your codebase for template literals inside query calls and replace every one.
5. Wildcard CORS Configuration
Access-Control-Allow-Origin: * tells browsers that any website on the internet can make authenticated requests to your API. This is the correct setting for a public CDN serving static assets. It is the wrong setting for an API that handles user data or performs mutations.
app.use(cors()); // defaults to wildcard: all origins accepted✅ only your own domain is allowedapp.use(cors({
origin: process.env.ALLOWED_ORIGIN || 'https://yourdomain.com',
credentials: true,
}));Lock origin to the specific domain your frontend runs on. Use an environment variable so you can set a different value in development without touching your production config.
6. Missing Rate Limiting on Public Endpoints
Any endpoint that is publicly accessible can be hammered. Without rate limiting, a single script can exhaust your database connections, rack up an unexpected API bill, or brute-force credentials by making thousands of attempts per minute. Login, signup, and password reset endpoints are the highest priority.
✅ rate limiter applied to the login routeimport rateLimit from 'express-rate-limit';
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 10, // 10 attempts per window
message: 'Too many login attempts. Please try again later.',
});
app.post('/api/auth/login', loginLimiter, loginHandler);If you are on Next.js without a custom server, you can implement the same logic using an in-memory store like upstash/ratelimit in your route handler or middleware.
7. Insecure Direct Object References (IDOR)
IDOR happens when you use a user-supplied ID to fetch a record without checking whether the requesting user actually owns that record. An attacker changes the ID in the URL from their own to someone else's and gets back data that was never meant for them. This is the vulnerability behind most "data exposure" breach headlines.
❌ fetches any record by ID without ownership checkexport async function GET(req: Request, { params }: { params: { id: string } }) {
const doc = await db.document.findUnique({ where: { id: params.id } });
return Response.json(doc);
}✅ ownership verified before returning dataexport async function GET(req: Request, { params }: { params: { id: string } }) {
const session = await getServerSession(authOptions);
if (!session) return new Response('Unauthorized', { status: 401 });
const doc = await db.document.findUnique({
where: { id: params.id, userId: session.user.id }, // must match session
});
if (!doc) return new Response('Not Found', { status: 404 });
return Response.json(doc);
}The fix is to always include the authenticated user's ID in the database query, not just the resource ID. If the record does not belong to the requesting user, it will not be returned.
8. Dependencies with Known CVEs
Your app is only as secure as the packages it depends on. A vulnerability in an npm package you installed six months ago can expose your users even if your own code is perfect. Running npm audit takes ten seconds and surfaces every known issue in your dependency tree.
# see all vulnerabilities
npm audit
# auto-fix the ones that can be patched without breaking changes
npm audit fix
# check what a major-version fix would change before applying
npm audit fix --dry-runFocus on critical and high severity issues first. If a fix requires a breaking upgrade, check the changelog before applying it. Set a reminder to run this monthly; new CVEs are disclosed continuously.
9. No HTTPS Enforcement
Serving over HTTP means credentials, session tokens, and user data travel unencrypted between your server and the browser. Most hosting platforms provision TLS automatically, but they do not always redirect HTTP traffic to HTTPS by default. Check that your platform redirects HTTP requests and that you have not disabled that setting anywhere.
If you are on Vercel, Netlify, or Railway, HTTPS and HTTP-to-HTTPS redirects are on by default. If you are managing your own server or using a reverse proxy like Nginx, verify the redirect is configured and test it with a plain HTTP request to your domain.
Also set the Strict-Transport-Security header so browsers remember to always use HTTPS for your domain, even if a user types the URL without it.
10. Missing Security Headers
HTTP security headers are a one-time addition that protects your users from a class of attacks browsers understand how to prevent. They are not set by default in most frameworks. A Content-Security-Policy blocks injected scripts, X-Frame-Options prevents clickjacking, and X-Content-Type-Options stops MIME sniffing attacks.
✅ security headers in next.config.tsconst securityHeaders = [
{ key: 'X-Frame-Options', value: 'SAMEORIGIN' },
{ key: 'X-Content-Type-Options', value: 'nosniff' },
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
{
key: 'Content-Security-Policy',
value: [
"default-src 'self'",
"script-src 'self' 'unsafe-inline'",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https:",
].join('; '),
},
{
key: 'Strict-Transport-Security',
value: 'max-age=63072000; includeSubDomains; preload',
},
];
// in your Next.js config:
headers: async () => [{ source: '/(.*)', headers: securityHeaders }]Start with X-Frame-Options, X-Content-Type-Options, and Strict-Transport-Security. Add a Content-Security-Policy once you have mapped out the external domains your app loads scripts and styles from.
Your Security Checklist at a Glance
Here is the complete list consolidated. Run through this before you send your first real user to the app.
Security Checklist
Secrets & Auth
Input & Injection
Transport & Headers
If you can check every item above, your repo is in better shape than the majority of apps that ship. That is a low bar, but it is the right one to clear first.
What to Do After the Checklist
A checklist you run manually is only as good as the last time you ran it. New code gets added, packages get updated, and the issues come back. The teams that stay secure are the ones that make these checks automatic, not occasional.
GitDoctor scans your GitHub repository and surfaces security issues, missing auth checks, injection risks, and dependency vulnerabilities with a severity rating and a ready-to-use AI prompt to fix each one. You get the kind of review a security-aware senior engineer would give, without scheduling a call or waiting for a code review slot.
Run it on your repo before you launch. It takes under a minute, and finding one critical issue before your users do is worth more than any feature you could ship in the same time.