Vibe Coding Security Series
- What Is Vibe Coding Security? A Field Guide for 2026
- The OWASP Top 10 for Vibe-Coded Applications
- Anatomy of a Vibe Coding Breach: Lessons from 2026’s Worst Incidents
- The Dependency Trap: Supply Chain Risks in AI-Generated Code
- Authentication & Secrets: What AI Gets Wrong Every Time
- Scanning Vibe-Coded Apps: Why Traditional SAST/DAST Falls Short
- Prompt Engineering for Secure Code
- The Founder’s Security Checklist (you are here)
- Securing the AI Coding Pipeline (coming soon)
- The Future of Vibe Coding Security (coming soon)
Read Time: 18 minutes
TL;DR
You built your MVP with AI. It works, users are signing up, and you’re thinking about launch. Before you do, run through these fifteen checks. They cover the vulnerabilities I see most often in vibe-coded apps — the ones that lead to data breaches, leaked credentials, and “we need to shut everything down” emails to your users. Each check has a test you can run in under five minutes, most from a browser or a single terminal command. Print the summary at the end and tape it next to your monitor.
Why This Checklist Exists
A founder I worked with shipped his vibe-coded MVP on a Thursday. By Saturday night his database was dumped — every user email, every record, everything. An attacker found the exposed MongoDB port, connected without credentials, and exfiltrated the lot. The founder had failed on three items from the list you’re about to read. It took him ten minutes to run the checks after the breach. It would have taken him ten minutes before.
I built the first version of this checklist at VULNEX after presenting at a security conference in 2025, based on vulnerabilities I kept seeing in AI-generated code. Since then, the pattern has only gotten worse. GitGuardian’s 2026 report found 28.65 million new secrets leaked on GitHub in 2025 — a 34% increase year over year. Commits involving AI coding assistants leak secrets at more than double the baseline rate. Apiiro’s research showed AI code adding over 10,000 new security findings per month across studied repositories by mid-2025. The breaches I covered in Part 3 — Moltbook, Enrichlead, apps breached within days of launch — all failed on items in this list.
This isn’t a comprehensive security program. It’s the fifteen things that, if you get them wrong, guarantee someone finds the hole before you do. If you get them right, you’re ahead of the vast majority of vibe-coded MVPs shipping today.
The checks are grouped into five areas. I’ll use QuickNote — the deliberately vulnerable note-taking app from earlier in this series — and a few other real-world examples to make each one concrete.
Area 1: The Perimeter
These are the things attackers see the moment they point a browser or a port scanner at your app.
Check 1: Force HTTPS on every page
AI-generated deployment configs routinely skip HTTPS. The model gives you a working Node.js app listening on port 3000 over plain HTTP — which is fine for local development and catastrophic in production. Without HTTPS, every login, every API token, every piece of user data travels across the internet in cleartext. Anyone on the same network — a coffee shop, a shared office, a compromised ISP — can read it.
How to test:
curl -I http://yourapp.com
You want a 301 or 308 redirect to https://. If you get a 200 on plain HTTP, your app is serving content without encryption. Also check that your API responds only on HTTPS — curl -I http://yourapp.com/api/notes should redirect, not return data.
How to fix: If you’re on Vercel, Netlify, or Cloudflare Pages, HTTPS is enforced automatically. On a VPS or Docker deployment, configure your reverse proxy (Nginx, Caddy) to redirect all HTTP to HTTPS. Caddy does this by default — one reason I recommend it for founders who don’t want to think about TLS certificates.
Check 2: Set security headers
Open securityheaders.com and scan your domain. If you get anything below a B, you have work to do. Across the web, only 21.9% of sites deploy a Content Security Policy — and vibe-coded apps are well below that average because AI rarely generates security header configuration unless you ask.
How to test:
curl -I https://yourapp.com | grep -iE "strict-transport|content-security|x-frame|x-content-type"
You want to see at least these four headers in the response: Strict-Transport-Security, Content-Security-Policy, X-Frame-Options, and X-Content-Type-Options. If you see none of them, your app has zero hardening against clickjacking, MIME sniffing, and protocol downgrade attacks.
How to fix: Add them in your reverse proxy, your Express middleware, or your hosting platform’s config. A reasonable starting set for an MVP:
Strict-Transport-Security: max-age=31536000; includeSubDomains
Content-Security-Policy: default-src 'self'; script-src 'self'
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()
Adjust Content-Security-Policy to match what your app actually loads — if you use a CDN for scripts, add its domain to script-src. If your app breaks after adding CSP (common with React apps that use inline scripts), start with script-src 'self' 'unsafe-inline' and tighten later. An imperfect CSP is better than no CSP.
Check 3: Close exposed ports and admin panels
AI deployment guides often leave database ports open to the internet. As of early 2026, Shodan indexes over 213,000 exposed MongoDB instances — many with no authentication required. If you’re using Firebase, don’t assume you’re safe: RedHunt Labs found that 1 in 5 Firebase databases had misconfigured rules allowing public read access, exposing emails, passwords, and private messages. Your database should never be reachable from the public internet — and “managed” doesn’t mean “secured.”
How to test:
nmap -Pn -p 5432,27017,6379,3306,9200 yourapp.com
That scans for PostgreSQL (5432), MongoDB (27017), Redis (6379), MySQL (3306), and Elasticsearch (9200). Every one of those ports should show filtered or closed. If any shows open, your database is directly accessible from the internet — and if it’s using default credentials or no auth (as Redis often does), it’s already compromised.
Also check for admin panels: browse to /admin, /dashboard, /supabase, /_next, /graphql, /phpmyadmin. If any of these load without requiring authentication from the public internet, lock them down or remove them.
How to fix: Configure your hosting provider’s firewall to allow database connections only from your application server’s IP. On AWS, that’s a security group rule. On a VPS, use ufw allow from <app-ip> to any port 5432. For admin panels, put them behind authentication or restrict access by IP.
Area 2: Secrets
The most common category of vibe coding vulnerability. AI generates code with secrets embedded in it because that’s what the training data shows — tutorial code hardcodes credentials for simplicity, and the model reproduces the pattern.
Check 4: Scan your codebase for hardcoded secrets
Of the 28.65 million secrets leaked on GitHub in 2025, a disproportionate share came from AI-generated code. GitGuardian found that commits involving an AI coding assistant leaked secrets at a 3.2% rate — more than double the 1.5% baseline across public GitHub. The model puts your Supabase service role key in a constant, your Stripe secret key in a config object, your database connection string in a Docker Compose file. It does this because that’s what works, and working code is what it optimizes for. Picture this: a founder pushes a Stripe secret key to a public repo at 2pm. By 4pm, bots have found it. By 6pm, fraudulent charges are hitting their account. This happens every day — GitGuardian’s data shows leaked secrets are typically exploited within hours of exposure.
How to test:
# Install and run Gitleaks on your repo
gitleaks detect --source . --report-format json --report-path leaks.json
Or use TruffleHog for deeper scanning including git history:
trufflehog git file://. --json
Any findings are secrets that have been committed to your repository. Even if you delete them from the current code, they’re in your git history — and if the repo was ever public, they’ve been scraped.
How to fix: Rotate every leaked secret immediately — don’t just remove it from code. Move all secrets to environment variables loaded at runtime. If you’re on Vercel, Railway, or Render, use their environment variable UI. Never put secrets in .env files that get committed to git. Which leads to the next check.
Check 5: Verify .env files and Docker images don’t leak secrets
Two hidden channels that AI routinely creates for secret leakage. First: .env files. The model creates a .env with your database credentials but doesn’t always add it to .gitignore. Second: Docker images. As I covered in Part 5, AI-generated Dockerfiles often bake secrets into the build with ARG and ENV instructions, making them visible in the image layer history.
How to test:
# Check if .env is in your gitignore
grep "\.env" .gitignore
# Check if any .env files are tracked by git
git ls-files | grep -i "\.env"
# Check Docker image for leaked secrets
docker history --no-trunc yourapp:latest | grep -iE "key|secret|password|token"
If git ls-files shows any .env file, that file — and every secret in it — is in your repository history. If docker history shows credentials, anyone who pulls your image can extract them.
How to fix: Add .env* to .gitignore before your first commit. For Docker, use multi-stage builds and pass secrets as runtime environment variables, never build arguments. If secrets are already in git history, you need to use git filter-repo to purge them — and rotate every exposed secret.
Check 6: Lock down CORS
Cross-Origin Resource Sharing misconfigurations are everywhere in vibe-coded apps. CORS issues consistently rank among the most common web application vulnerabilities, and vibe-coded apps are especially prone because the typical AI-generated Express.js setup includes cors() with no arguments — which defaults to Access-Control-Allow-Origin: *, allowing any website on the internet to make authenticated requests to your API.
How to test:
curl -H "Origin: https://evil.com" -I https://yourapp.com/api/notes
Look at the Access-Control-Allow-Origin header in the response. If it says * or reflects back https://evil.com, your API will happily serve data to any website that asks — including an attacker’s phishing page.
How to fix: Configure CORS to allow only your own domains:
app.use(cors({
origin: ['https://yourapp.com', 'https://www.yourapp.com'],
credentials: true
}));
Never use origin: true (reflects any origin) or leave CORS at the default wildcard in production.
Area 3: Authentication and Access
This is where vibe-coded apps fail hardest. The AI builds authentication that works — you can log in, you see your data — but it skips the controls that prevent everyone else from seeing your data too. I covered the details in Part 5, but here’s how to test for the critical failures.
Check 7: Add rate limiting to login and signup
Without rate limiting, your login endpoint accepts unlimited password attempts. Credential stuffing — automated attacks using leaked username/password pairs from other breaches — generates 26 billion attempts per month globally. Microsoft Entra blocks 7,000 password attacks per second. If your login has no rate limit, an attacker can try thousands of passwords per minute against your users’ accounts.
QuickNote had this exact vulnerability. No rate limiter on /api/login meant an attacker could brute-force any account password at the speed of their internet connection.
How to test:
# Send 20 rapid requests to your login endpoint
for i in $(seq 1 20); do
curl -s -o /dev/null -w "%{http_code}\n" \
-X POST https://yourapp.com/api/login \
-H "Content-Type: application/json" \
-d '{"email":"test@test.com","password":"wrong"}';
done
If all 20 return 401 (invalid credentials) with no 429 (too many requests), you have no rate limiting. You should start seeing 429 responses after 5-10 attempts.
How to fix: In Express.js, add express-rate-limit:
const loginLimiter = rateLimit({
windowMs: 60 * 1000,
max: 5,
message: { error: 'Too many attempts, try again later' }
});
app.post('/api/login', loginLimiter, loginHandler);
Apply rate limiting to signup and password reset endpoints too — those are targeted just as often.
Check 8: Verify every API endpoint checks authentication
AI-generated APIs often have authentication on some endpoints but not others. The model builds a login flow, generates a token, and then forgets to check that token on half the routes. I’ve reviewed vibe-coded apps where /api/login was properly secured but /api/users, /api/notes, and /api/admin accepted unauthenticated requests.
How to test:
# Try hitting your API endpoints with no authentication token
curl -s https://yourapp.com/api/notes
curl -s https://yourapp.com/api/users
curl -s https://yourapp.com/api/settings
Every protected endpoint should return 401 Unauthorized when called without a valid token. If any of them return data, that endpoint is publicly accessible to anyone who knows the URL.
How to fix: Add authentication middleware that runs on every route by default, then explicitly exempt only public routes (login, signup, health check). In Express.js:
// Exempt public routes BEFORE the auth middleware
app.post('/api/login', loginHandler);
app.post('/api/signup', signupHandler);
// Then apply auth middleware to everything else under /api
app.use('/api', authMiddleware);
Check 9: Test that users can only access their own data
This is the IDOR vulnerability — Insecure Direct Object Reference — and it’s the single most dangerous flaw in multi-tenant vibe-coded apps. The app works correctly when you use it normally: you see your notes, your invoices, your profile. But if you change the ID in the URL or API request, you see someone else’s data. QuickNote had this: changing /api/notes/42 to /api/notes/43 returned another user’s private notes. No ownership check, no authorization — just a database lookup by ID.
How to test:
# Log in as user A, get their token, and note the ID of a resource they own
# Then try accessing a resource that belongs to user B
curl -H "Authorization: Bearer <user-a-token>" \
https://yourapp.com/api/notes/9999
If this returns data (instead of 403 Forbidden), any authenticated user can access any other user’s data by guessing or incrementing IDs. If your app uses auto-incrementing integer IDs, an attacker can enumerate every record in your database.
How to fix: Add a WHERE user_id = authenticated_user_id clause to every database query. If you’re on Supabase, enable Row Level Security and create policies:
CREATE POLICY notes_owner ON notes
USING (user_id = auth.uid());
Test the policy by logging in as two different users and verifying that neither can see the other’s data.
Area 4: Data Handling
How your app processes what users send it. AI-generated code is optimistic by default — it assumes all input is well-formed and trustworthy. Attackers don’t send well-formed input.
Check 10: Validate all input on the server
If your app has a form, test what happens when you put <script>alert('xss')</script> in every text field. If your app has a search feature, try '; DROP TABLE users; --. AI-generated code almost never validates input server-side unless you specifically ask for it. Client-side validation (HTML required attributes, JavaScript checks) is trivially bypassed — open the browser dev tools and delete the validation, or send requests directly with curl.
Imagine you built a freelancer invoicing app with AI. The “company name” field in the invoice form probably accepts any string. An attacker puts a script tag in the company name, generates an invoice, and when your client opens that invoice PDF or web view — the script executes in their browser, potentially stealing their session.
How to test:
# Test for XSS in a text field
curl -X POST https://yourapp.com/api/notes \
-H "Authorization: Bearer
<token>" \
-H "Content-Type: application/json" \
-d '{"title":"<script>alert(1)</script>","content":"test"}'
# Test for SQL injection in a search parameter
curl "https://yourapp.com/api/search?q=test%27%20OR%201=1--"
If the script tag is stored and rendered back without escaping, you have stored XSS. If the SQL injection test returns more data than expected, you have SQL injection.
How to fix: Validate and sanitize all input server-side. Use a validation library like Zod or Joi in Node.js. Define what each field should accept — data type, max length, character set — and reject anything that doesn’t match. Sanitize HTML with a library like DOMPurify before rendering user-generated content.
Check 11: Use parameterized queries
This is the server-side defense against SQL injection. String-concatenated queries — where user input is glued directly into the SQL string — are one of the oldest and most dangerous vulnerabilities in web development. AI generates them regularly because the training data is full of them.
How to test:
# Search your codebase for string concatenation in SQL
grep -rn "query.*\`.*\${" ./src/
grep -rn "query.*+.*req\." ./src/
grep -rn "f\".*SELECT" ./src/
Any match is a potential SQL injection vulnerability. The pattern query(\SELECT FROM notes WHERE id = ${noteId}`)is vulnerable. The patternquery(‘SELECT FROM notes WHERE id = $1′, [noteId])` is safe.
How to fix: Replace every string-concatenated query with parameterized queries. In Node.js with pg:
// Vulnerable
db.query(`SELECT * FROM notes WHERE id = ${noteId}`);
// Safe
db.query('SELECT * FROM notes WHERE id = $1', [noteId]);
If you’re using an ORM like Prisma or Drizzle, you’re mostly safe by default — but check for any $queryRawUnsafe or $executeRawUnsafe calls, which bypass ORM protections.
Check 12: Don’t store tokens or sensitive data in localStorage
This is the vulnerability that gives an attacker full account takeover through any XSS hole. localStorage is accessible to every script running on your page. If an attacker finds any way to inject JavaScript — through a stored XSS in a user profile field, through a compromised third-party script, through a browser extension — they can read every token in localStorage and send it to their server.
QuickNote stored JWT access tokens in localStorage. Combined with the missing input validation, this meant any XSS vulnerability gave an attacker every user’s authentication token.
How to test:
Open your app in the browser, log in, then open Developer Tools (F12) → Application → Local Storage. If you see anything labeled token, access_token, jwt, session, or similar — that’s a finding. Also check sessionStorage.
How to fix: Store authentication tokens in httpOnly cookies with Secure and SameSite=Strict flags. These cookies are invisible to JavaScript — XSS can’t read them, and they’re sent automatically with every request to your server. This is what the security-aware prompt in Part 7 produces by default.
Area 5: Dependencies and Deployment
What you shipped alongside your own code. AI tools pull in dependencies you never chose, generate configurations you never reviewed, and create error handling that tells attackers exactly what went wrong.
Check 13: Audit your dependencies for known vulnerabilities
Every dependency your AI tool added is an attack surface you didn’t consciously accept. Sonatype’s 2026 report documented 454,648 new malicious packages in 2025 — a 75% increase year over year. Your AI coding assistant chose packages based on training data popularity, not on whether they’ve been patched recently or whether they’ve been flagged as malicious.
How to test:
# Node.js
npm audit
# Python
pip-audit
# Or use Snyk for a more detailed report
npx snyk test
npm audit is built into Node.js and runs in seconds. Pay attention to high and critical severity findings. pip-audit does the same for Python. For a deeper analysis including transitive dependencies and reachability, Snyk and Endor Labs offer free tiers.
How to fix: Run npm audit fix for automatic patches. For vulnerabilities that can’t be auto-fixed, check if a newer version of the package resolves them, or find an alternative package. I covered the full dependency management workflow in Part 4.
Check 14: Lock down file uploads
If your app accepts file uploads — profile pictures, documents, attachments — test what happens when you upload something that isn’t what the form expects. Unrestricted file uploads are a CVSS 10.0 vulnerability class. In April 2025, CVE-2025-31324 — an unauthenticated file upload in SAP NetWeaver — was exploited in the wild to upload webshells and achieve full remote code execution. The same pattern appears in vibe-coded apps: AI generates an upload endpoint that saves whatever it receives to the filesystem, no type checking, no size limit, no filename sanitization.
How to test: Try uploading a file with a .html or .svg extension through your app’s upload form. If it’s saved and accessible at a public URL, try accessing it in a browser — if the HTML renders or the SVG executes JavaScript, you have a stored XSS via file upload. Also test uploading a very large file (100MB+) — if there’s no size limit, that’s a denial-of-service vector.
How to fix: Validate file type on the server by checking the file’s magic bytes, not just the extension (extensions can be faked). Limit file size. Store uploads in a dedicated storage bucket (S3, Cloudflare R2) with a content-type override that forces downloads rather than rendering. Never serve user-uploaded files from the same domain as your application — use a separate subdomain or CDN domain.
Check 15: Make sure errors don’t leak internal details
AI-generated code leaves detailed error messages in production. Stack traces, database connection strings, file paths, package versions — all information that helps an attacker understand your infrastructure and find their next exploit. The default Express.js error handler, for example, sends the full stack trace to the client in development mode — and AI-generated code often doesn’t switch to production mode on deployment.
How to test:
# Trigger an error by requesting a resource that doesn't exist
curl https://yourapp.com/api/notes/nonexistent-id-999999
# Try sending malformed data
curl -X POST https://yourapp.com/api/notes \
-H "Content-Type: application/json" \
-d '{"invalid json'
If the response includes a stack trace, file paths (like /app/src/routes/notes.js:42), database errors (like relation "users" does not exist), or framework version numbers — your error handling is leaking information.
How to fix: Set NODE_ENV=production in your deployment environment. Add a global error handler that catches all errors and returns a generic message to the client while logging the details server-side:
app.use((err, req, res, next) => {
console.error(err); // Logged server-side, not sent to client
res.status(500).json({ error: 'Internal server error' });
});
The Printable Checklist
Print this. Tape it next to your monitor. Run through it before every deploy. Download the one-page PDF version if you want a cleaner printout.
The Perimeter
- 1. HTTPS forced on every page —
curl -I http://yourapp.comreturns 301/308 redirect - 2. Security headers set — securityheaders.com score B or higher
- 3. No exposed database ports or admin panels —
nmap -p 5432,27017,6379shows filtered/closed
Secrets
- 4. No hardcoded secrets —
gitleaks detectreturns zero findings - 5. .env excluded from git, no secrets in Docker layers —
git ls-files | grep .envreturns nothing - 6. CORS locked to your domains —
curl -H "Origin: https://evil.com"doesn’t reflect origin
Authentication & Access
- 7. Rate limiting on login/signup — 20 rapid requests trigger 429 responses
- 8. Every API endpoint requires authentication — unauthenticated curl returns 401
- 9. Users can only access their own data — cross-user ID test returns 403
Data Handling
- 10. Server-side input validation —
<script>tags rejected or escaped - 11. Parameterized queries —
grepfinds no string-concatenated SQL - 12. No tokens in localStorage — browser dev tools show no auth tokens in storage
Dependencies & Deployment
- 13. Dependencies audited —
npm auditshows zero high/critical findings - 14. File uploads restricted — type, size, and storage location validated
- 15. Errors don’t leak details — malformed requests return generic messages, no stack traces
If you can only fix three things today
If you ran the checklist and failed on multiple items, here’s where to start:
First: Check 4 (hardcoded secrets). If Gitleaks found secrets in your repo, they’re already leaked. Every minute you wait is a minute an attacker can use those credentials. Rotate them now — before fixing anything else.
Second: Check 9 (users accessing other users’ data). If your IDOR test passed, any authenticated user can browse your entire database by incrementing IDs. This is the vulnerability that turns a security incident into a data breach notification.
Third: Check 1 (HTTPS). Without HTTPS, every fix you apply afterward can be intercepted in transit. HTTPS is the foundation — nothing else works without it.
Everything else matters, but these three are the ones where the gap between “vulnerable” and “breached” is measured in hours, not weeks.
What This Checklist Doesn’t Cover
Fifteen items can’t cover everything. This checklist is the floor, not the ceiling. A few things you’ll need beyond this list as you grow past MVP:
Penetration testing. Once you have paying users, hire a professional to try to break in. At VULNEX we do this kind of work regularly, and I can tell you that a pentest almost always finds things no checklist catches — business logic flaws, race conditions, trust boundary issues that only surface when a human thinks like an attacker against your specific application.
Logging and monitoring. Check 7 tells you to add rate limiting, but you also need to know when someone is probing your defenses. Log authentication attempts, data access patterns, and error rates. Ship logs to a service that can alert you when patterns change.
Compliance. If you handle health data (HIPAA), payment card data (PCI DSS), or European user data (GDPR), you have regulatory requirements beyond this checklist. Don’t assume AI-generated code is compliant — check.
Automated scanning. This checklist is manual. Once you’ve passed it, set up automated security scanning in your CI/CD pipeline — SAST, DAST, dependency checks on every pull request. I covered why vibe-coded apps need different scanner configurations than traditional code in Part 6.
Threat modeling. Part 7 covered how to build a threat model before writing code. If you skipped that step, go back and do it now. The checklist catches common issues; a threat model catches the ones specific to your application.
The One Thing to Remember
Every check in this list exists because I’ve seen a vibe-coded app fail on it in production. Not in theory — in production, with real user data exposed. The QuickNote vulnerabilities from this series, the breaches from Part 3, the authentication failures from Part 5 — they all map to items on this list.
AI built your app. It didn’t secure it. That’s your job, and this checklist is the minimum. Run it before launch. Run it again after every major feature. Make it a habit, and your vibe-coded MVP will be more secure than most traditionally coded apps I audit.
As always: trust nothing, verify everything.
- X (Twitter): @SimonRoses
Further Reading
- What Is Vibe Coding Security? A Field Guide for 2026 — Part 1 of this series
- The OWASP Top 10 for Vibe-Coded Applications — Part 2 of this series
- Anatomy of a Vibe Coding Breach: Lessons from 2026’s Worst Incidents — Part 3 of this series
- The Dependency Trap: Supply Chain Risks in AI-Generated Code — Part 4 of this series
- Authentication & Secrets: What AI Gets Wrong Every Time — Part 5 of this series
- Scanning Vibe-Coded Apps: Why Traditional SAST/DAST Falls Short — Part 6 of this series
- Prompt Engineering for Secure Code — Part 7 of this series
References
- GitGuardian (2026). The State of Secrets Sprawl 2026.
- Apiiro (2025). 4x Velocity, 10x Vulnerabilities: AI Coding Assistants Are Shipping More Risks.
- HTTP Archive (2025). Web Almanac 2025 — Security.
- Sonatype (2026). State of the Software Supply Chain.
- Akamai (2024). State of the Internet — Credential Stuffing.
- RedHunt Labs (2022). Analysing Misconfigured Firebase Apps — Project Resonance Wave 10.
- NIST NVD (2025). CVE-2025-31324 — SAP NetWeaver Unrestricted File Upload.


