In February 2025, Andrej Karpathy talked about something he called "vibe coding": building software by describing what you want to an AI and accepting the result without looking at the code. Within a year this became the way not just weekend projects, but products with real users, get built.
Here's the problem: AI is very good at writing code that works. The login screen opens, the form saves, the payment page loads. But the distance between "it works" and "it's secure" is as large as the distance between a demo and a live product. And since nobody writes that distance into the prompt, the model doesn't close it either.
In this post I've grouped the 19 steps I check before putting an application live into six layers. For each one, you'll find what AI typically does, what you should do, and a sentence you can add to your prompt. Let's start with the numbers.
Why isn't "it works" enough?
A language model is trained to produce what it's asked for. When you say "let users log in", you get a screen where users can log in. Unless you say "limit login attempts, store passwords with Argon2, and don't reveal whether a username exists on a failed login", those things most likely won't come. Security is a requirement nobody asks for but everyone assumes.
Veracode's 2025 report showed this is measurable: when they could choose between a secure and an insecure path, models chose the insecure one in 45% of cases. More striking still, while models wrote syntactically better code every year, their security success stood still.
There's a human side too. A team from Stanford had participants solve the same security tasks with and without an AI assistant:
- Noticeably less secure code
- More likely to believe their code was secure
- Fewer flaws among those who trusted the assistant less and refined their prompts
- More secure solutions
- More cautious about their own code
- The same five tasks; Python, JavaScript and C
The most dangerous point is the second one: overconfidence. Because the code arrives fast, we assume it arrived correct. 2025 showed the price of that assumption plainly, more than once.
- MarchEnrichLead shut down
A SaaS product whose founder announced he had built it "without writing a single line of code by hand" was attacked shortly after: API keys were in the front end, the subscription check was bypassed, and the keys' quotas were drained. The product was shut down.
Secret management · Authorization · Rate limiting - MayCVE-2025-48757 · Lovable
Missing Row-Level Security policies in generated apps let unauthenticated people read and write tables. Of 1,645 apps one researcher scanned, about 170 had their data exposed.
IDOR · Database security - JulyA Replit agent deleted a production database
Despite an instruction "not to make changes without approval", the agent deleted a database containing 1,206 executive records and more than 1,196 company records, then said rollback was impossible. The data was restored from backup.
Database security · Backup - JulyThe Tea app
About 72,000 images leaked from a Firebase storage bucket left open to the public; about 13,000 of them were verification selfies and ID photos. A second dataset containing 1.1 million private messages surfaced afterwards.
File upload · Authorization - 2025 · ResearchEscape.tech: 5,600 apps
They passively scanned more than 5,600 public apps built with vibe coding platforms: more than 2,000 vulnerabilities, more than 400 exposed secrets and 175 exposures of personal data (health records, IBANs, phone numbers).
All of them
None of these incidents was a zero-day or a state-sponsored attack. Each was the absence of one or more of the basic checks in the list below.
19 checks, 6 layers
Rather than memorising the list item by item, it helps to think in layers. Even if one layer is bypassed, another should stand behind it.
This list isn't random. Every item on OWASP's Top 10, published in November 2025 and based on data from more than 2.8 million applications, is covered by at least one of these 19 checks:
Layer 1 · Identity and access
01 Authentication
Authentication answers the question "who are you?". In Verizon's 2025 report, the most common starting point for breaches is still stolen or misused credentials (22%). In other words, the door usually isn't broken down; it's opened with a key.
Writes the login system from scratch. Stores passwords in plain text, MD5 or unsalted SHA-256. Signs JWTs with a key like "secret" and no expiry. Reveals that an account exists by saying "this email isn't registered".
Use the framework's built-in auth system or a mature provider. Store passwords with Argon2id or bcrypt. Enforce two-factor authentication on admin accounts. Put an expiry on sessions and keep error messages generic.
02 Authorization
Authentication tells you who someone is; authorization tells you what they can do. "Broken Access Control" was first on the OWASP list in 2021, and it's first again in 2025. The most common mistake in vibe-coded apps is doing the permission check only in the interface: the admin button isn't shown to normal users, but the endpoint is open to everyone. That's exactly how the paid features were bypassed at EnrichLead.
The rule is simple: every request is denied by default, on the server; what is explicitly allowed gets checked.
# Bad: the check lives only in the front end
@api_view(["DELETE"])
def delete_course(request, pk):
Course.objects.filter(pk=pk).delete()
# Good: on the server, on every request
@api_view(["DELETE"])
@permission_classes([IsAuthenticated, IsCourseAdmin])
def delete_course(request, pk):
course = get_object_or_404(Course, pk=pk, organization=request.user.organization)
course.delete()
IBM's 2025 report shows this applies to AI systems themselves too: 97% of organisations that suffered a breach of an AI model or application had no AI-specific access controls.
03 IDOR and ownership checks
If you can open /api/invoices/1043, you can try 1044 too. If the server doesn't ask "does this invoice belong to this user?", you'll see someone else's invoice. This is called IDOR (Insecure Direct Object Reference), and it's one of the flaws automated scanners find hardest to catch and attackers find easiest.
# Bad: returns any invoice
invoice = Invoice.objects.get(pk=pk)
# Good: query limited to the owner; 404 for anyone else's
invoice = get_object_or_404(Invoice, pk=pk, owner=request.user)
Using UUIDs makes guessing harder, but it doesn't replace an ownership check. If the front end talks directly to the database (Supabase, Firebase), this check must be done at the database level with Row-Level Security. The Lovable case shows what happens when that policy is missing.
Layer 2 · Input and output
04 Input validation
Validation in the browser is for user experience, not security. An attacker doesn't use your form; they send the request directly. Every input must be validated on the server for type, length, format and range. Use an allowlist where possible: not "what should I block?" but "what should I accept?"
A commonly skipped kind is mass assignment: a serializer that accepts all of a model's fields opens the door for a user to add "is_admin": true to the request.
class ProfileSerializer(serializers.ModelSerializer):
class Meta:
model = Profile
fields = ["display_name", "bio", "avatar"] # list them explicitly
read_only_fields = ["role", "is_verified"]
05 SQL injection protection
If you use an ORM, you're largely protected. AI, however, tends to switch to raw SQL exactly where the ORM isn't enough (dynamic filters, search and sorting) and to embed the value in the string.
# Bad: user input becomes part of the query
cursor.execute(f"SELECT * FROM products WHERE name LIKE '%{q}%'")
# Good: parameterised query
cursor.execute("SELECT * FROM products WHERE name LIKE %s", [f"%{q}%"])
# A sort column can't be a parameter: use an allowlist
order = {"price": "price", "date": "created_at"}.get(sort, "created_at")
06 XSS protection
In Veracode's tests, writing code secure against XSS was where models struggled most: they failed in 86% of cases.
Modern template engines (Django, Jinja, React) escape output by default. The flaw appears where you deliberately turn that protection off: |safe and mark_safe in Django, dangerouslySetInnerHTML in React, v-html in Vue, innerHTML in plain JavaScript. If you have to display rich text, sanitise the HTML with an allowlist (nh3 in Python, DOMPurify in the browser) and put a Content Security Policy behind it.
This blog works the same way: the Markdown I write is sanitised with nh3 before it reaches the page. Even if my admin account were taken over, a <script> couldn't be embedded in a post.
07 File upload validation
An "upload a profile photo" feature means giving people the right to write files to your server. Checking the extension isn't enough; photo.jpg.php, or an SVG with a script inside, gets through an extension check.
- Validate the file's content (magic bytes, or by actually trying to open the image).
- Set a size limit; generate a random file name instead of using the name the user gave.
- Keep files outside the web root or in separate storage; never execute them.
- Check the storage bucket's access rules. In the Tea case, the leak came from an open bucket nobody was looking at.
Layer 3 · Browser and transport
08 HTTPS
Today HTTPS isn't a choice; it's the default. Let's Encrypt is free, and platforms such as Vercel, Render and Railway set up certificates automatically. What you need to check: that HTTP requests are redirected to HTTPS, that the HSTS header stops the browser from trying HTTP again, and that if the application sits behind a proxy, it knows the request really arrived over HTTPS.
SECURE_SSL_REDIRECT = True
SECURE_HSTS_SECONDS = 31536000
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
09 Secure cookies
The session cookie is the key to the account. Three flags protect that key:
| Flag | What it does |
|---|---|
HttpOnly |
JavaScript can't access the cookie; an XSS flaw can't steal the session. |
Secure |
The cookie is only sent over HTTPS. |
SameSite=Lax |
The cookie isn't sent on requests from other sites; reduces CSRF risk. |
AI often writes the token to localStorage. That's an area every script on the page can access; a single XSS flaw takes the session with it. Keeping the session in an HttpOnly cookie is a safer default. Don't forget to rotate the session ID after login, either.
10 CORS configuration
CORS isn't a wall that stops attackers. It's the set of rules for relaxing the browser's restriction on "reading from a different origin". When AI sees a CORS error, it suggests the fastest fix: allow_origins=[""]. Since doesn't work with cookies, the next step is to reflect the incoming Origin header back as-is. That means any site can send requests to your API on your user's behalf and read the response.
Access-Control-Allow-Origin: * or reflecting the incoming Origin, plus Allow-Credentials: true on top.
An explicit allowlist: https://myapp.com, https://admin.myapp.com. Development origins only in the development environment.
11 Security headers
A few HTTP headers give the browser the answer to "what is allowed on this page?". They take ten minutes to set up and close off a whole range of attack classes from the start.
| Header | What it prevents |
|---|---|
Content-Security-Policy |
Loading scripts from unapproved sources; limits the impact of XSS. |
Strict-Transport-Security |
Downgrade attacks from HTTPS to HTTP. |
X-Content-Type-Options: nosniff |
The browser guessing a file type and running a script. |
frame-ancestors / X-Frame-Options |
The page being put in an invisible frame on another site (clickjacking). |
Referrer-Policy |
Information in the URL leaking to other sites. |
Permissions-Policy |
Unnecessary use of APIs such as camera, microphone and location. |
You can check the result with securityheaders.com or Mozilla Observatory.
Layer 4 · Application behaviour
12 Rate limiting
A login form without limits is a door where thousands of passwords can be tried every second. An AI endpoint without limits is a model someone else runs on your bill. That's why the API keys' quotas ran out at EnrichLead.
Places you should set limits: login, password reset, OTP verification, sign-up, anything that sends email or SMS, and expensive operations (LLM calls, file conversion, exports). On this site, admin login allows 5 failed attempts per IP every 15 minutes, and PDF generation is limited to 10 requests a minute.
13 Global exception handling
In 2025, OWASP opened a separate category for the first time: Mishandling of Exceptional Conditions. It has two risks. The first is an error telling the user too much: stack traces, SQL queries, file paths, library versions. The second is subtler: the system failing open when an error occurs. Code that lets a request through when the permission check throws an exception is more dangerous than code with no check at all.
14 Logging and audit logs
Every day a breach goes unnoticed is a day handed to the attacker. Logs are the only answer to "what happened?". But wrong logging is a risk too: in Veracode's tests, models produced insecure code against log injection in 88% of cases; writing user input to the log without sanitising it opens the way to forged log lines.
- Record: successful and failed logins, permission denials, admin actions, data exports, settings changes. For every record: who, what, when, from where, and the outcome.
- Don't record: passwords, tokens, full card numbers, more personal data than necessary.
- Keep logs somewhere separate from the application server and set alerts on critical events.
On this site, every login attempt goes to a separate security log; the visitor counter never stores IP addresses, only a salted hash.
Layer 5 · Data and secrets
15 Secret management
API keys, database passwords, payment provider keys. Getting these into the code is the most frequent and most expensive mistake in vibe coding.
There's another trap specific to vibe coding: in front-end frameworks, every variable prefixed with NEXT_PUBLIC_ or VITE_ is embedded in the bundle sent to the browser. When AI defines a variable with this prefix "to make it work", your secret key ends up in a JavaScript file anyone can see.
- Secrets go in
.env, and.envgoes in.gitignore. In production, the platform's secret manager. - Every call that needs a secret key is made from the server; the browser only talks to your backend.
- Set up pre-commit scanning (gitleaks, ggshield).
- If a secret has leaked, deleting the commit isn't enough: revoke the key immediately and generate a new one.
16 Database security
Can the application's database user drop tables? Is the database open to the internet? Are development and production the same database? In the Replit case, the answer to the last question was "yes". Afterwards, the platform started separating development and production databases automatically.
- Least privilege: the application user should only be able to read and write the tables it needs; schema changes go through a separate user.
- The database should only be reachable from the application's network; connections over TLS.
- If the front end connects to the database directly, RLS or security rules must be enabled on every table.
- Don't give AI agents production database credentials. Let the agent work in the development environment; every change that goes to production should pass human approval.
17 Backups
Backups are the check that kicks in on the day the other 16 checks fail. According to Sophos's 2024 research, the average cost of recovering from a ransomware attack, excluding the ransom, is 2.73 million dollars. And the only reason the Replit case had a happy ending was that a backup existed.
Decide two numbers up front: how much data loss you can tolerate (RPO) and how quickly you need to be back up (RTO). Your backup frequency and method follow from those two answers.
Layer 6 · Supply chain and process
18 Dependency scanning
Most of the code you ship isn't actually yours; it belongs to the packages you install. Over one year starting in November 2023, Sonatype detected 512,847 malicious open-source packages, a 156% increase on the year before.
AI has added a new risk to this: packages that don't exist. A study published at USENIX Security 2025 examined 576,000 code samples produced by 16 models. About one in five suggested packages didn't actually exist. And because the same made-up names were suggested again and again, attackers can register those names in advance and put malicious code inside. This is called slopsquatting.
- Before installing any package AI suggests, look at its registry page: does it really exist, who publishes it, how many downloads does it have, and how long has it been around?
- Commit the lock file (
package-lock.json,poetry.lock) to the repo. - Continuously scan for known vulnerabilities with
npm audit,pip-audit, OSV-Scanner and Dependabot or Renovate.
19 CI/CD security scanning
The last check makes sure the others aren't done once and forgotten. A pipeline that runs on every push catches what people miss and, more importantly, stops new code from breaking what was already there.
# .github/workflows/security.yml
name: security
on: [push, pull_request]
permissions:
contents: read
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: pip install pip-audit bandit
- run: pip-audit -r requirements.txt
- run: bandit -r . -x ./tests -ll
The pipeline itself is an attack surface too: define CI secrets with least privilege, pin third-party actions to a version or commit, and write the permissions field explicitly.
Where to start?
Nineteen items can look daunting. You don't need all of them at once; but going live without some of them is like leaving the door open and going on holiday.
- Authentication
- Authorization
- IDOR / ownership
- Input validation
- SQL injection
- XSS
- HTTPS
- Secret management
- Database security
- Backups
- Secure cookies
- CORS
- Security headers
- Rate limiting
- Global exception handling
- File upload
- Logging / audit
- Dependency scanning
- CI/CD scanning
One last suggestion: instead of having AI write security, ask it. After the code is generated, give this list to the same model in a separate conversation and ask it to audit its own code. When the generating model and the auditing perspective are separated, a good share of what slipped through in the first round gets caught. Still, the final decision should rest with a human who reads the code.
AI is a very fast and very eager helper. But code written fast is code that goes live without being read. Don't let speed get ahead of security.
Sources
- Veracode, 2025 GenAI Code Security Report, July 2025.
- N. Perry, M. Srivastava, D. Kumar, D. Boneh, Do Users Write More Insecure Code with AI Assistants?, ACM CCS 2023.
- J. Spracklen et al., We Have a Package for You! A Comprehensive Analysis of Package Hallucinations by Code Generating LLMs, USENIX Security 2025.
- GitGuardian, The State of Secrets Sprawl 2025.
- GitGuardian, The State of Secrets Sprawl 2026 (summary by The Hacker News), March 2026.
- OWASP, OWASP Top 10:2025.
- IBM, Cost of a Data Breach Report 2025, July 2025.
- Verizon, 2025 Data Breach Investigations Report.
- Sonatype, 10th Annual State of the Software Supply Chain Report, October 2024.
- Sophos, The State of Ransomware 2024.
- NIST NVD, CVE-2025-48757.
- Escape.tech, How we discovered over 2k high-impact vulnerabilities in apps built with vibe coding platforms.
- OECD AI Incidents Monitor, Replit AI coding tool deletes live production database, July 2025.
- BleepingComputer, Tea app leak worsens with second database exposing user chats, July 2025.
- Indie Hackers, Vibe coding has a security problem, 2025.