All posts
SecurityMay 18, 20267 min read

GitHub Repo Security Checklist: 10 Things to Fix Before You Ship

Security is the first thing vibe coders skip and the last thing they think about until something goes wrong. Most repos ship with at least three of the issues on this list. Work through it before you hand your URL to real users.

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 source
const stripe = new Stripe('sk_live_4eC39HqLyjWDarjtT7en');
const openai = new OpenAI({ apiKey: 'sk-proj-abc123...' });
✅ secret loaded from environment
const 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 returned
export 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 database
export 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 schema
import { 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 SQL
const results = await db.query(
  `SELECT * FROM orders WHERE user_id = '${req.query.userId}'`
);
✅ parameterized query keeps input and code separate
const 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.

❌ any origin allowed
app.use(cors()); // defaults to wildcard: all origins accepted
✅ only your own domain is allowed
app.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 route
import 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 check
export 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 data
export 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-run

Focus 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.ts
const 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

Move all API keys to environment variables
Add authentication to every private route
Check IDOR: validate ownership before serving data
Rotate any key that has ever touched a commit

Input & Injection

Validate and sanitize all user inputs
Use parameterized queries everywhere
Restrict CORS to specific allowed origins
Add rate limiting to all public endpoints

Transport & Headers

Enforce HTTPS and redirect HTTP traffic
Set Content-Security-Policy header
Set X-Frame-Options and X-Content-Type-Options
Run npm audit and patch critical CVEs

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.

Scan your repo

Find the security issues before attackers do

GitDoctor runs 70+ checks across security, auth, injection risks, and dependency health on your GitHub repo. Each finding comes with an AI prompt to fix it. First scan is free.

Scan your repo