---
title: "05 — Security Best Practices — You Ship What the AI Wrote, Including the Holes"
uid: vibe-coding-security-best-practices
tags: ["secrets", "security", "vibe-coding", "roadmap:vibe-coding", "ai", "audit"]
excerpt: "AI generates code that looks correct — and 'looks correct' is the wrong standard for security. Plausible code can have real holes; the secrets, audit, and accountability are still yours."
date: 2026-08-13T03:27:25+0000
source: https://www.aveshina.my.id/en/blog/vibe-coding-security-best-practices
---

Security is the easiest thing to skip when the build is flying, which is exactly when the holes get in. Writing it down forced one idea to the front: **the AI generates code that looks correct, and "looks correct" is the exact wrong standard for security.** A working login form can still leak tokens. A functioning database layer can still be injectable. Plausibility hides holes, and the holes are mine when they ship [1]. The roadmap's framing is direct: _security is easy to overlook when you're moving fast_ — so the oversight has to be deliberate.

The model that clicked: **vibe coding compresses the time it takes to ship a vulnerability.** What used to take a junior a week to write (and a reviewer a day to catch) now takes an AI five minutes to generate and ships ten minutes later if no one's looking. The speed multiplies on both sides — more features per hour, and more holes per hour, unless I build the discipline in.

## Why AI-generated code has a security shape

There are three reasons AI code lands with a particular security profile, and naming them is the first defense:

- **It's trained on public code, including bad public code.** The model has ingested millions of tutorials, and a lot of tutorial code is insecure by design — simplified to teach a concept, with auth stripped out, secrets hardcoded, queries concatenated. The model reproduces those patterns because they're common, not because they're safe [2].
- **It optimizes for the request, not for threats.** When I ask for "a login endpoint," the model builds one that works. It rarely adds rate limiting, account lockout, timing-attack resistance, or secret rotation unless I ask for them — because I didn't ask, and the model's job is to satisfy the request.
- **It doesn't know my threat model.** The AI has no idea whether this endpoint faces the public internet or an internal network, whether this secret is a test key or production, whether this input is user-controlled or trusted. Threat-relevant context has to be supplied, or the code defaults to "assume benign input."

```figure
<svg viewBox="0 0 700 260" xmlns="http://www.w3.org/2000/svg" class="my-6 w-full max-w-2xl" role="img" aria-label="An AI-generated login form on the surface looks fine. A cutaway beneath reveals three classes of hidden issues: hardcoded secrets in the code, no rate limiting on the endpoint, and a concatenated SQL query. To the right, two defenses: env vars for secrets and a pre-launch AI security audit.">
  <g font-family="ui-sans-serif, system-ui, sans-serif" text-rendering="geometricPrecision">

    <rect x="40" y="30" width="280" height="60" rx="8" fill="#e0e7ff" stroke="#6366f1" stroke-width="1.5"/>
    <text x="180" y="52" font-size="12" font-weight="700" fill="#1e1b4b" text-anchor="middle">AI-generated login form</text>
    <text x="180" y="70" font-size="10" fill="#1e1b4b" text-anchor="middle">"looks correct" — works on the happy path</text>

    <text x="180" y="116" font-size="10" font-weight="700" fill="#7f1d1d" text-anchor="middle">hidden issues (cutaway)</text>

    <rect x="40" y="128" width="280" height="36" rx="6" fill="#fee2e2" stroke="#dc2626" stroke-width="1.5"/>
    <text x="180" y="144" font-size="10" font-weight="700" fill="#7f1d1d" text-anchor="middle">hardcoded secret</text>
    <text x="180" y="158" font-size="9.5" font-family="ui-monospace, monospace" fill="#7f1d1d" text-anchor="middle">API_KEY = "sk-live-9f2a…"</text>

    <rect x="40" y="170" width="280" height="36" rx="6" fill="#fee2e2" stroke="#dc2626" stroke-width="1.5"/>
    <text x="180" y="186" font-size="10" font-weight="700" fill="#7f1d1d" text-anchor="middle">no rate limiting / lockout</text>
    <text x="180" y="200" font-size="9.5" fill="#7f1d1d" text-anchor="middle">brute-forceable forever</text>

    <rect x="40" y="212" width="280" height="36" rx="6" fill="#fee2e2" stroke="#dc2626" stroke-width="1.5"/>
    <text x="180" y="228" font-size="10" font-weight="700" fill="#7f1d1d" text-anchor="middle">concatenated query</text>
    <text x="180" y="242" font-size="9.5" font-family="ui-monospace, monospace" fill="#7f1d1d" text-anchor="middle">"... WHERE u='" + input + "'"</text>

    <text x="490" y="64" font-size="11" font-weight="700" fill="#052e16" text-anchor="middle">defenses</text>

    <rect x="380" y="80" width="280" height="60" rx="8" fill="#dcfce7" stroke="#16a34a" stroke-width="1.5"/>
    <text x="520" y="102" font-size="11" font-weight="700" fill="#052e16" text-anchor="middle">environment variables</text>
    <text x="520" y="120" font-size="10" fill="#052e16" text-anchor="middle">secrets never in source</text>

    <rect x="380" y="160" width="280" height="60" rx="8" fill="#dcfce7" stroke="#16a34a" stroke-width="1.5"/>
    <text x="520" y="182" font-size="11" font-weight="700" fill="#052e16" text-anchor="middle">pre-launch security audit</text>
    <text x="520" y="200" font-size="10" fill="#052e16" text-anchor="middle">explicitly ask AI to find holes</text>

    <path d="M322,146 C350,146 352,110 378,110" fill="none" stroke="#64748b" stroke-width="1.5" marker-end="url(#aarrow)"/>
    <path d="M322,190 C350,190 352,190 378,190" fill="none" stroke="#64748b" stroke-width="1.5" marker-end="url(#aarrow)"/>
    <defs>
      <marker id="aarrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">
        <path d="M0,0 L10,5 L0,10 z" fill="#64748b"/>
      </marker>
    </defs>
  </g>
</svg>
```

The good news is that the AI is also a useful security reviewer — once I ask. The same model that wrote the hole can often find it when I explicitly request an audit. The asymmetry is that it won't offer; I have to ask.

## Secrets: environment variables, always

The first concrete habit the roadmap names, and the cheapest to keep: _always use environment variables for sensitive information like API keys and passwords_ [1]. Hardcoded secrets are the single most common AI-introduced vulnerability, because the model writes the key inline to make the example self-contained, and the example becomes the implementation.

The rule I keep:

- Secrets live in .env files (gitignored) and reach the code through process.env.
- The AI never sees production secrets. I paste test keys into prompts, never live ones.
- Before commit, a pre-commit hook scans for high-entropy strings (strings random enough to look like a secret). Tools exist; I don't write my own.

The pattern is mechanical, and once it's in place, the AI follows it for the rest of the project — but only if I set it before the first secret appears.

## The pre-launch audit: explicitly ask for holes

The second roadmap habit, and the one I most often skipped: _before going live, explicitly ask AI to audit your app for vulnerabilities_ [1]. The word _explicitly_ is doing all the work. A general "review this code" prompt gets a style review. A targeted "audit this for security vulnerabilities — list every one you find, with severity and fix" prompt gets a security review [3].

A useful audit prompt names the categories I care about:

```
Audit this codebase for security vulnerabilities. For each finding report:
- category (authn/authz, injection, secrets, headers, deps, SSRF, etc.)
- severity (critical / high / medium / low)
- file + line
- concrete fix
Do not skip findings to be polite. I want them all.
```

The AI won't catch everything — it's a starting point, not a pentest [3]. But it catches a surprising amount, and it does it in minutes. Pairing its output with a dependency scan (known CVEs in the lockfile) and a secret scan covers most of what a pre-launch check needs.

## The honest ceiling

I want to be clear about what this is and isn't. An AI security audit is not a substitute for a real pentest (penetration test), a dependency policy, or threat modeling on anything that handles money, identity, or sensitive data. It's a high-value first pass that catches the common stuff fast. For anything high-stakes — payments, auth, medical, regulated data — the AI audit is _in addition to_ human review, not instead of it. The roadmap's guidance is for the long tail of apps where "no glaring holes before launch" is a reasonable bar. It is not the bar for a bank.

## How I use this

Three habits, in order. First, secrets in environment variables before the first key is written, with a pre-commit secret scan as a backstop. Second, a targeted pre-launch audit prompt that asks for every finding by category and severity, run as a separate session from the build. Third, honest scoping — for anything high-stakes I treat the AI audit as a first pass, not a sign-off, and pair it with human review. The shortcut I never take: shipping AI code to production without an explicit security pass, because "it works" and "it's safe" are different sentences.

## References

[1] roadmap.sh, "Security Best Practices," 2026. [Online]. Available: [https://roadmap.sh/vibe-coding/security-best-practices](https://roadmap.sh/vibe-coding/security-best-practices)

[2] Databricks, "Passing the Security Vibe Check: The Dangers of Vibe Coding," 2026. [Online]. Available: [https://www.databricks.com/blog/passing-security-vibe-check-dangers-vibe-coding](https://www.databricks.com/blog/passing-security-vibe-check-dangers-vibe-coding)

[3] Legit Security, "Vibe Coding Security: Risks and Best Practices," 2026. [Online]. Available: [https://www.legitsecurity.com/aspm-knowledge-base/vibe-coding-security](https://www.legitsecurity.com/aspm-knowledge-base/vibe-coding-security)

```quiz
Q: Why does AI-generated code land with a particular security shape?
- the model is designed to be insecure
- it's trained on public code including bad patterns, optimizes for the request not threats, and doesn't know your threat model
correct: 1
explain: Tutorials often strip auth and hardcode secrets for clarity; the model reproduces those common patterns. It satisfies the literal request without adding defenses you didn't ask for.

Q: Where should secrets live in an AI-coded project?
- hardcoded near the code that uses them, for convenience
- in gitignored .env files, reached via process.env — never in source
correct: 1
explain: Hardcoded secrets are the most common AI-introduced vulnerability because the model writes keys inline to make examples self-contained. Env vars plus a pre-commit secret scan is the mechanical fix.

Q: Why does a general "review this code" prompt fail as a security audit?
- it doesn't; the model always audits for security by default
- it gets a style review — you must explicitly ask for vulnerabilities by category and severity
correct: 1
explain: The model optimizes for the request. A general review gives general feedback. A targeted "audit for vulnerabilities, list every one with severity and fix" prompt is what produces a security pass.

Q: The honest ceiling on an AI security audit is…
- a complete substitute for human review on any app
- a high-value first pass that catches common issues fast — not a pentest or a sign-off for high-stakes apps
correct: 1
explain: AI audits catch a surprising amount quickly but miss things too. For payments, auth, medical, or regulated data, pair the AI pass with human review — don't treat it as a sign-off.

Q: Vibe coding's effect on security timelines is best described as…
- it has no effect; vulnerabilities ship at the same rate as before
- it compresses the time to generate and ship vulnerabilities, unless discipline is built in
correct: 1
explain: What used to take a week to write and a day to review now takes minutes to generate and ships ten minutes later. Speed multiplies on both sides — features and holes — without explicit defenses.
```
