BlogEngineering

Security Checks Every AI-Built App Needs Before Launch

AI coding tools write functional code quickly. They also reproduce the same security gaps consistently — not because the models are careless, but because the training data reflects real-world patterns, and real-world code has these problems.

This is not a criticism of LLMs. It's a practical observation: if you build with Cursor, Claude Code, GitHub Copilot, or any similar tool, the same five issues will appear in your output with high regularity. Here's what to check before you ship.

1. Exposed API keys in HTTP responses

The most common and most expensive finding in AI-built apps.

LLM-generated code frequently includes API keys in comments as examples, in .env files without proper .gitignore setup, or in server-side code that accidentally gets bundled client-side. The specific patterns to check:

Next.js / React apps: Any variable starting with NEXT_PUBLIC_ is bundled into client-side JavaScript and visible to every visitor. Check your .env file for:

# These get embedded in the browser bundle:
NEXT_PUBLIC_OPENAI_KEY=sk-...        # Never do this
NEXT_PUBLIC_STRIPE_SECRET=sk_live_... # Never do this
NEXT_PUBLIC_DATABASE_URL=postgres://  # Never do this
 
# This is fine — not a secret:
NEXT_PUBLIC_API_URL=https://api.example.com

Express / Node.js apps: Check that dotenv is loaded before any import that uses the variables, and that your API client is server-only:

// WRONG — process.env may not be loaded yet in some bundler configurations
import openai from './openai'; // openai.ts reads OPENAI_KEY at module init
require('dotenv').config();
 
// CORRECT — load env before anything uses it
require('dotenv').config();
const openai = require('./openai');

Check in browser DevTools: Open your app, open Network tab, search every response for your key prefix (sk-, AKIA, ghp_, etc.). AI-generated code has put secrets in API responses, HTML comments, and JavaScript source maps.

2. Missing security headers

Every major framework — Next.js, Express, FastAPI, Django — has zero security headers by default. AI tools rarely add them unless you explicitly ask. Each missing header is a class of attack you're unnecessarily exposed to.

Run your deployed URL through the PatchVex scanner or curl -I https://your-app.com to check. Here's what a secure response looks like vs a typical AI-generated app:

# Typical AI-generated app (newly deployed):
HTTP/2 200
content-type: text/html
x-powered-by: Express   ← tells attackers your stack

# What you want:
HTTP/2 200
content-type: text/html
strict-transport-security: max-age=31536000; includeSubDomains
content-security-policy: default-src 'self'; script-src 'self' 'nonce-abc'
x-content-type-options: nosniff
x-frame-options: SAMEORIGIN
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=()

The fastest fix for Next.js:

// next.config.ts — add these headers to every response
async headers() {
  return [{
    source: '/(.*)',
    headers: [
      { key: 'X-Content-Type-Options', value: 'nosniff' },
      { key: 'X-Frame-Options', value: 'SAMEORIGIN' },
      { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
      { key: 'Strict-Transport-Security', value: 'max-age=31536000; includeSubDomains' },
      { key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' },
    ],
  }];
}

Session cookies without Secure, HttpOnly, and SameSite flags are trivially hijackable in several common scenarios:

  • No HttpOnly → any JavaScript on the page can read the cookie (XSS → session theft)
  • No Secure → cookie sent over HTTP connections (network interception)
  • No SameSite → cookie sent on cross-site requests (CSRF attacks)

LLM-generated auth code often sets cookies correctly in the happy path and incorrectly in edge cases. Check all Set-Cookie headers in your Network tab:

# Insecure (common AI output):
Set-Cookie: session=abc123; Path=/

# Secure:
Set-Cookie: session=abc123; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=604800

For Next.js with cookies():

import { cookies } from 'next/headers';
 
const cookieStore = await cookies();
cookieStore.set('session', token, {
  httpOnly: true,
  secure: process.env.NODE_ENV === 'production',
  sameSite: 'lax',
  maxAge: 60 * 60 * 24 * 7,
  path: '/',
});

4. CORS misconfiguration

Access-Control-Allow-Origin: * on authenticated endpoints is a common AI-generated pattern. The model learned it from tutorials that prioritize making things work over security.

// WRONG — common AI output for "fix CORS error"
app.use(cors()); // allows all origins
 
// Also WRONG — still too broad:
app.use(cors({ origin: '*' }));
 
// CORRECT — explicit allowlist:
const ALLOWED_ORIGINS = ['https://yourapp.com', 'https://www.yourapp.com'];
app.use(cors({
  origin: (origin, callback) => {
    if (!origin || ALLOWED_ORIGINS.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error('Not allowed by CORS'));
    }
  },
  credentials: true, // required if using cookies
}));

Access-Control-Allow-Origin: * is incompatible with Access-Control-Allow-Credentials: true. If your API uses session cookies or Authorization headers, * won't work anyway — and you should be using an allowlist.

5. Open cloud storage

Supabase, Firebase, S3, and similar services are set up by AI tools with the fastest working configuration — which is often "public read." Check these manually:

Supabase: Dashboard → Storage → check that each bucket has appropriate RLS policies. New buckets default to private, but AI-generated code sometimes explicitly sets them to public:

-- AI-generated code you might find:
CREATE POLICY "Public read" ON storage.objects FOR SELECT USING (true);
-- This makes everything in the bucket public

AWS S3: Check bucket ACLs and bucket policy. Block Public Access should be enabled unless you're intentionally serving public content:

aws s3api get-public-access-block --bucket your-bucket-name
# Should show all four settings as "true"

Firebase Firestore: Check your security rules. The default for a new project is often open:

// INSECURE — anyone can read/write everything:
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write: if true;
    }
  }
}

6. Rate limiting on auth endpoints

AI-generated login/register/password-reset endpoints rarely include rate limiting. Without it, your authentication system is open to:

  • Credential stuffing (automated testing of leaked credential pairs)
  • Password brute force
  • Account enumeration via response timing

For Express:

const rateLimit = require('express-rate-limit');
 
const authLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 10,                    // 10 attempts
  message: { error: 'Too many attempts, try again in 15 minutes' },
  standardHeaders: true,
  legacyHeaders: false,
  skipSuccessfulRequests: true, // don't count successful logins
});
 
app.use('/api/auth/', authLimiter);

The 5-minute pre-launch check

Before you ship any AI-built app:

  1. Scan the deployed URL with PatchVex Web Scanner — catches headers, cookies, CORS, and secret exposure automatically
  2. Search the bundle for secrets — open DevTools → Network → search for sk-, AKIA, ghp_, Bearer, your app's domain name in unexpected places
  3. Check every Set-Cookie header in Network tab — verify HttpOnly; Secure; SameSite on session cookies
  4. Test CORS — from browser console: fetch('https://your-api.com/api/user', {credentials:'include'}).then(r=>r.json()).then(console.log) from a different domain
  5. Check cloud storage — manually verify your S3 bucket, Supabase storage, or Firebase storage is not publicly readable unless intentional

The PatchVex scanner covers items 1–4 automatically. Item 5 requires manual verification because it requires authentication.

Ship fast, but run these checks first. A security report after launch is much more expensive than one before it.