
Vibe Coding's Blind Spots: A Security Audit of AI-Generated JavaScript
Why AI-Generated JavaScript Needs a Security Pass
AI-generated JavaScript is usually fast, readable, and just wrong enough to be dangerous. It often works in a happy-path demo, which is why it gets merged. The trouble shows up later, when the app meets real users, real permissions, and real data.
I review generated code the same way I review rushed human code: assume the UI is lying until the backend proves otherwise. That catches most of the damage.
The main issue with vibe coding is not syntax. It is trust. Generated code tends to trust the browser, trust client state, trust request bodies, and trust defaults that look harmless until they become access control bugs.
What Vibe Coding Misses in Practice
Unsafe defaults and hidden trust boundaries
Generated code often picks the easiest path: localStorage for auth state, isAdmin flags in React state, or a frontend-only gate before a sensitive action. Those choices feel practical because they reduce friction.
They also create a fake boundary. If the server never re-checks the action, the client-side check is theater.
Client-side checks that never reach the server
A common failure pattern is “disable the button for free users.” That is not authorization. It is presentation.
If the API accepts the same request from any authenticated account, a free user can often replay the request directly. The UI never had authority in the first place.
Overbroad data access and accidental privilege leaks
Generated code also tends to fetch too much data and filter later in the browser. That is a quiet leak. Even if the UI hides a field, the response still contained it.
That matters when the payload includes billing info, internal IDs, drafts, or other users' records. Redaction belongs where the data is assembled, not where it is displayed.
A Practical Audit Workflow for Generated Code
Start with data flow and entry points
I start by tracing three things:
- Where data enters the app.
- Where it is stored.
- Where it is used to make a decision.
If a generated component reads from localStorage, query params, or an untrusted API response and then changes authorization, I treat that as a review hotspot.
Draw the request path first. Most security mistakes are easier to spot as data-flow problems than as “buggy code.”
Trace authorization, not just validation
Validation answers “is this input shaped correctly?” Authorization answers “is this user allowed to do this?”
Generated code often does the first one and skips the second. A form might validate projectId, but the API still needs to confirm that the current user owns that project.
Look for dangerous browser APIs and dynamic execution
The obvious red flags are still red flags:
evalnew Function()innerHTMLdangerouslySetInnerHTML- string-built script URLs
Sometimes the code is not obviously malicious. It just builds HTML from user content or assembles code from fragments. That is enough to create XSS or logic injection.
Small Code Examples That Fail Real Tests
Token storage and session handling mistakes
// Looks convenient, but this is a weak pattern.
localStorage.setItem("token", token);
export function isLoggedIn() {
return Boolean(localStorage.getItem("token"));
}This fails a basic threat model. Any script running in the page can read that token. The app also treats “token exists” as proof of identity, which is not the same thing as a verified session.
Use an HttpOnly session cookie when you can, and verify the session on the server for every protected route.
API calls that trust the UI too much
async function upgradeUser(userId) {
if (!window.confirm("Upgrade this user?")) return;
await fetch("/api/admin/upgrade", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ userId }),
});
}The confirm dialog is irrelevant. If the endpoint trusts the caller just because the button was hidden in the UI, any authenticated user may be able to hit it directly.
The fix is backend authorization: verify the role, the resource, and the action.
Unsafe use of eval, Function, or unescaped HTML
function renderPreview(userInput) {
preview.innerHTML = userInput;
}
function runSnippet(code) {
return Function(`return (${code})`)();
}Both lines are bad for different reasons. The first can execute injected HTML or script. The second turns input into executable code.
If you need preview rendering, sanitize with a real allowlist-based approach. If you need a calculator or rule engine, use a parser or a constrained interpreter, not raw JavaScript execution.
How to Reduce Risk Without Slowing Development
Add secure-by-default prompts and review checklists
The easiest defense is to make the model work inside a safer template. I ask for:
- server-side authorization checks
- HttpOnly cookies for sessions
- no
evalorFunction - no
innerHTMLunless sanitized - explicit ownership checks for every resource mutation
That prompt alone removes a lot of bad defaults.
Use linting, dependency checks, and test cases
Static checks help because generated code repeats patterns. Add rules that catch risky APIs and force review for escaped HTML or insecure storage choices.
Then write tests for the security contract, not just the UI contract:
- free user cannot call admin endpoint
- user cannot read another user's record by changing an ID
- session expires correctly
- HTML preview does not execute scripts
Keep sensitive logic on the backend
This is the part people skip when they want speed. But the backend is where trust belongs.
If a feature affects permissions, billing, ownership, or secrets, the frontend should only request it. The backend should decide it. That separation keeps generated code useful without letting it define your security model.
Conclusion
AI-generated JavaScript is fine for scaffolding, UI glue, and repetitive work. It becomes risky when it starts making trust decisions.
My rule is simple: if the code decides who may do what, or what data they may see, I audit it like production security code. The fast path is still useful. It just needs a security pass before it becomes real.


