Shipping fast without shipping holes

Does "vibe coding" leave security holes? (Yes, here's where)

You described what you wanted, the model wrote it, it ran, and you shipped it. The feature works. That isn't luck. The models are genuinely good at producing working code.

Here's the gap nobody mentions in the demo. The AI was optimizing for one thing: make this work. Keeping things from leaking is a separate job, and you didn't ask for it, so it didn't happen. The model wrote the feature and never checked what it left open. So yes, vibe coding leaves holes. The useful question is which ones, and they cluster in a few predictable places.

Where the holes actually land

Three categories show up again and again in code that was generated fast and shipped without a review pass: leaked secrets, debug mode left on, and files that got deployed by accident. None of them are exotic. All of them are visible from outside.

Secrets, hardcoded and shipped

Ask a model to "connect to Stripe" or "set up the Supabase client" and it will write code that works the moment you paste your key in. The fastest path to working is a key sitting right there in the file. So that's often what you get: a sk_live_, a Supabase service_role JWT, an AKIA… AWS key, dropped straight into a component or a config file.

The model has no idea whether that file ends up on the server or in the browser bundle. It frequently picks the client-side spot, because that's where the example in its training data lived. Mark it with a NEXT_PUBLIC_ or VITE_ prefix, which a model will cheerfully suggest, and your framework inlines that secret into the bundle it serves to every visitor.

yoursite.com
SourcesConsoleNetwork
searchservice_role
app.4f1ed1.min.js
!function(e){var t={};function n(r){...e.exports}return n(0)}
,a.key="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.••••.svc_role",
t.charge=function(e){return r.post("/v1/charges",e)}
1 match - your live secret key is in the bundle.
A Supabase service_role key in the shipped bundle bypasses every row-level security rule you wrote.

A service_role key is the worst case here, because it ignores the row-level security you carefully set up. Whoever finds it in your bundle reads and writes your whole database as an admin. The model gave you a working connection and said nothing about where the key ended up.

Debug mode, left on

"Help me debug this" is most of what vibe coding is. So the generated app is very often configured for the developer's convenience: DEBUG=True in Django, dev-mode error pages in Rails, a Werkzeug or Whoops interactive traceback. Great while you're building. A liability the instant it's public.

An attacker triggers an error on your live site on purpose, then reads the response. A debug traceback hands them file paths, your framework version, fragments of config, environment variables, and in the Werkzeug case, an interactive Python console in the browser. The model set up the dev experience you asked for and never flipped it back for production, because production was never the prompt.

request
GET /api/orders/not-a-number HTTP/1.1
response
HTTP/1.1 500 Internal Server Error
Traceback (most recent call last):
File "/srv/app/views.py", line 88, in get_order
DEBUG = True # settings.py, line 12
SECRET_KEY = 'django-insecure-8f3k...'
DEBUG left on turns any error into a readable map of your app, config and secret key included.

Files that rode along

Generated projects accumulate scaffolding: a .env the model created for you, a seed.sql or backup.sql from when you were testing data, a .git directory, sometimes an /.aws/credentials file it referenced in an example. When you deploy the whole folder, those go up too. Then a bot requests /.env and /backup.sql directly, and if your host serves them, the model's "helpful" setup file is now a public download.

Why the model can't catch this for you

It's tempting to think the next prompt will fix it. Sometimes it does. But the model is reasoning about the code in front of it, not about your deployed surface. It can't see what your host actually serves, whether your bundle shipped the key, or whether debug got toggled in the environment you deployed to. Those are properties of the running system, not the source file.

That's the mismatch. The exposure is a deploy-time fact, and the model works at edit time. You can ask it to review the code and it'll make reasonable suggestions, but it's guessing about the part that matters, which is what a stranger gets when they hit your real URL.

See it the way an attacker does

Keep using AI. Just check the output where the holes actually appear, which is at the live URL, not in the source file. All three of these failures are visible from the public internet, and that's exactly what SurfaceCheckr looks at. It requests the files that shouldn't be there, pulls the JavaScript bundle you really shipped and searches it for key patterns, and triggers errors to see whether a debug page answers. Same vantage point a stranger has, because that's the point.

One honest limit: a passive external scan catches the exposed key, the debug traceback, and the reachable .env, but it can't tell you whether the logic the AI wrote is correct, and it won't log into anything. For that you want a pentest, which we don't pretend to be. For everything else a fast-shipped product tends to leave open, the MVP exposure rundown covers the rest.

The model wrote you a feature in minutes. Spend three checking what it shipped alongside it.

Find it before someone else does.

Paste your domain. The grade and issue count are free, and you'll see in a couple of minutes exactly what's reachable from outside.