Friday, September 4, 2026

The Seven Security Problems That Keep Appearing in Vibe-Coded Applications

 

Vibe-coded applications have a very recognizable failure pattern. The first version appears quickly; the interface looks good; the major features work; and everybody involved gets that pleasant feeling that months of development have collapsed into a weekend. Then somebody other than the person who built it starts using the application, does something completely ordinary that was never part of the happy path, and suddenly the project becomes considerably more interesting.

The problem is usually not that the AI produced incomprehensible garbage. Quite the opposite, because most of these applications look surprisingly reasonable when first inspected. The problem is that AI coding systems are extremely good at creating the expected path through an application. At the same time, security failures tend to live in the paths nobody demonstrated, nobody specified, and nobody bothered trying until the application had real users.

The recurring issues are also remarkably conventional. Exposed secrets, missing server-side authorization, cross-user data access, weak monitoring, untested recovery, client-controlled business logic, and unexpected code changes are not new vulnerability classes invented by artificial intelligence. What AI changes is how quickly these problems can be created, combined, and pushed into something that looks finished enough to deploy.

A collection of real-world observations used in this project identifies essentially the same seven failures appearing repeatedly in AI-built applications: secrets committed into code, interfaces acting as the only security boundary, users accessing one another's information, missing error tracking, backups that have never been restored, payment logic trusting client input, and AI-generated rewrites that change things outside the intended area.

1. Secrets End Up Somewhere They Should Never Have Been

Secrets are probably the least surprising item on the list, yet they continue to show up because they are incredibly convenient during development. The application needs an API key, database credentials, an authentication secret, a cloud token, or a third-party service password, so someone provides it to the AI agent to get the integration working. Five minutes later, the feature works, everyone moves on, and the credential quietly becomes part of the project.

AI makes this particularly easy because credentials can enter the workflow through several routes. A developer can paste it directly into a prompt, place it in a configuration file that the agent reads, expose it through an environment variable, or include it in logs while troubleshooting an authentication problem. The agent can then reproduce the value in sample code, tests, documentation, configuration, command examples, or debugging output without having any malicious intention whatsoever.

Consider a simple API integration. The application needs to call an external service, so the developer gives the coding agent a working token and asks it to “get the API working.” The agent creates a configuration module, inserts the token, verifies that the request succeeds, and generates a nice little example showing how everything works.

The feature is now functional, but the credential may be sitting in the repository.

Replacing the visible token afterward does not necessarily fix the problem either. Git history may still contain the original value, build artifacts may have copied it, or somebody may already have cloned the repository while the secret was present. Once a real secret is committed, the safe assumption is that it needs to be rotated rather than merely deleted.

This is where secret handling needs to be defined before the integration task begins. The agent should know that real credentials do not belong in source code, documentation, tests, or example configurations, and that the environment should prevent unnecessary production secrets from being exposed during unrelated work. Telling the AI not to leak credentials is useful, but keeping those credentials outside its context is a much stronger control.

A quick defensive check is almost embarrassingly simple. Search the repository for strings such as password, secret, token, apikey, Authorization, and common key prefixes used by services involved in the project. Then inspect configuration history and generated documentation as well, because the embarrassing secret sometimes disappears from the current source while remaining comfortably preserved three commits earlier.

2. The Interface Becomes the Security System

This one is dangerous because the application can look completely correct during ordinary use. A non-administrator logs in and does not see the administrative button, privileged menu, delete option, or configuration panel, so everybody concludes that authorization is working. Unfortunately, hiding something in the interface is not the same thing as preventing somebody from calling the function directly.

Attackers do not have to use the interface.

If the browser normally sends a request such as:

DELETE /api/users/184

there is nothing stopping someone from sending that same request manually with curl, Burp Suite, Postman, browser developer tools, or a small script. If the server assumes that anyone reaching the endpoint must have seen the appropriate button first, the entire authorization model is built around good manners.

AI-generated applications often fall into this pattern because interface logic is visible and easy to demonstrate. “Only show this button to administrators” produces an immediate visual result. At the same time, “enforce role authorization independently on every protected server operation” is less exciting and may never be tested unless it was explicitly required.

Consider an internal reporting application with an administrator-only export function. The UI checks the user's role and hides the Export All button from everyone except administrators, but the backend route simply accepts an authenticated session. A normal user who discovers /api/admin/export can retrieve the same information because the interface performed the authorization decision instead of the server.

The fix is straightforward in concept. Authorization belongs where the protected action occurs, and the server should verify it regardless of how the request arrived. The interface can still hide controls for usability, but that should be treated as a presentation choice rather than an enforcement.

This is the difference between putting a “Staff Only” sign on a door and actually locking it. The sign communicates intent, while the lock determines whether the boundary survives somebody who does not care about the sign. Security needs the lock.

3. One User Can See Another User's Data

Broken object-level authorization is one of the most useful tests to run on any vibe-coded application with accounts. Create two users, give the first user some records, authenticate as the second user, and attempt to retrieve the first user's information by changing an identifier. The frequency with which this works is unpleasantly educational.

The application may correctly require authentication and still completely fail authorization. A route such as /api/projects/7721 might verify that the requester is logged in, retrieve project 7721, and return it without ever checking whether that project belongs to the authenticated account. Authentication answered “Who are you?” while the application forgot the second question: “Are you allowed to see this?”

Suppose User A creates a private security finding and receives object ID 2418. User B creates another finding and receives 2419, which tells us something useful immediately because sequential identifiers make guessing extremely easy. User B changes the request back to 2418, and the server returns the first finding because ownership was never checked.

The flaw becomes even more serious when write operations behave the same way. Changing an object identifier in an update or delete request may allow one user to modify or remove another user's information. At that point, the application does not merely leak data; it allows account boundaries to be crossed through perfectly valid API requests.

AI coding systems are particularly likely to create this problem when asked to build CRUD functionality quickly. Create, read, update, and delete operations are highly patterned, and the model can generate them effortlessly. The subtle part is ensuring that every operation enforces ownership, role membership, tenancy, or whatever authorization rule actually governs the object.

The easiest defensive test is also one of the best. Build two accounts and deliberately behave badly with one of them. Change identifiers, replay requests, try update and delete operations, and ask what the server knows about ownership at the exact moment it returns the object.

If the answer is “the UI normally prevents that,” there is more work to do.

4. Nobody Knows the Application Is Failing

Vibe-coded projects often devote a great deal of attention to making the application work and almost no attention to determining how anyone will know when it stops working. During development, this seems harmless because the person building the application is sitting directly in front of it. Errors appear in the terminal, browser console, agent output, or whatever development environment is being used.

Users do not reliably report bugs, and many simply leave when something behaves strangely. Background jobs can fail silently, API calls can start returning errors, authentication can become unreliable, database operations can time out, and scheduled processes can stop running without producing anything visible in the interface.

Consider a report-generation feature that calls an external AI API. When the provider changes the response format, the application's parser begins throwing exceptions, and the interface responds with a generic “Unable to generate report” message. The user tries again twice, assumes the application is unreliable, and moves on.

Meanwhile, nobody maintaining the application knows there is a problem.

Logging and error tracking are therefore part of making an application operational rather than optional polish added after launch. The system should record failures with enough context to diagnose what happened while avoiding sensitive information that does not belong in logs. Authentication failures, authorization denials, application exceptions, dependency failures, and important administrative actions deserve particular attention.

This does not require building a giant monitoring platform for a small application. Structured logs, centralized error tracking, health checks, and a few meaningful alerts can provide enormous value. The goal is simply to make important failures visible somewhere other than the user's face.

There is an old operational reality hidden here: if the only way to discover a service failure is for somebody to call and complain, the users are the monitoring system. They are usually very expensive monitoring sensors and have terrible documentation.

5. Backups Exist, but Recovery Does Not

Almost everybody says they have backups. The more interesting question is whether anyone has restored one.

A backup file sitting somewhere is not proof that recovery works. The file may be incomplete, encrypted with a missing key, incompatible with the current application version, missing uploaded files, missing external configuration, or damaged in a way nobody notices until the moment it matters. A backup that has never been restored is really a theory about recovery.

Vibe-coded applications can make this worse because the architecture may evolve quickly. A simple application begins with SQLite, moves to PostgreSQL, starts storing documents in object storage, adds a vector database, adds environment-specific configuration, and eventually depends on several external services. The original backup routine may continue copying a single database file while half of the application's state now lives elsewhere.

Consider a small internal knowledge application. The database is backed up every night, so everyone feels comfortable until the server fails and the restore begins. The database comes back, but uploaded documents were stored in a local directory that was never included in the backup process.

The metadata says the documents exist.

The documents themselves are gone.

Testing recovery reveals these gaps while the system is still healthy. Restore the data to an isolated environment, start the application, authenticate, retrieve records, verify uploaded content, check the configuration, and confirm that the restored system behaves as expected. That exercise also exposes undocumented dependencies that may have quietly accumulated as the AI agent expanded the application.

Backup design should follow the application's actual state, not the architecture everyone remembers from three months ago. Every significant new storage mechanism should trigger the question of whether backup and restore procedures still cover it.

A restore test can be inconvenient.

Discovering during an incident that the backup never contained the important data is slightly more inconvenient.

6. The Client Gets Trusted With Decisions It Should Never Own

Another recurring failure occurs when the browser or mobile client supplies information that the server should calculate or independently verify. Prices are a common example because an interface might calculate the total and then send that value to the backend as part of the purchase request. Everything works beautifully as long as nobody changes the request.

The client is controlled by the user.

Anything arriving from it should therefore be treated as input rather than authority. A hidden field, a disabled button, a JavaScript variable, a price value, a role indicator, or an object owner supplied by the client can be modified before the request reaches the server.

Imagine an application selling access to a premium feature for $99. The browser sends:

{
  "plan": "premium",
  "price": 99
}

The server accepts the value and creates the payment request.

Changing the request to:

{
  "plan": "premium",
  "price": 1
}

should not create an unexpected discount program, but it absolutely can if the backend trusts the client-supplied price.

The secure design has the server determine the authoritative price from the selected product or plan. The client can specify what the user wants to purchase, but it should not determine what that item costs. The same principle applies to permissions, ownership, discounts, account status, workflow state, and other values that affect security or business logic.

Webhooks create another related issue. Applications may accept payment or status callbacks without properly verifying signatures because the happy path works during development. An attacker who can imitate the callback format may then be able to create false success events unless the server verifies that the message actually came from the expected provider.

The broader rule is simple enough to remember. The client can make requests, but the server needs to make decisions.

Anything important enough to protect should be independently verified somewhere the user cannot rewrite with browser tools.

7. The Agent Changes Something Nobody Asked It to Change

This is one of the more distinctly AI-flavored problems on the list. An agent is asked to fix one part of an application and, while doing so, notices another component that could be “improved.” It refactors a helper, updates a library, adjusts styling, rewrites a configuration file, changes an API response, or modifies another component because doing so appears to produce a cleaner solution.

Sometimes the additional work really is better.

The problem is that nobody asked for it.

Consider an agent tasked with fixing an authentication timeout. It updates the session code as expected, but while inspecting the project it also notices that several components use an older API pattern. The agent helpfully modernizes them, updates a dependency, changes three tests, and restructures a utility module.

The authentication problem is fixed.

Two days later, somebody discovers that an unrelated export function behaves differently because the utility refactor changed how timestamps are formatted.

This is why AI-generated changes need bounded scope and reviewable diffs. The faster an agent can modify code, the easier it becomes to create more change than a human can realistically inspect. A forty-file rewrite may take the model two minutes and still require somebody to understand forty files before the change deserves trust.

Regression tests help enormously here. If five application workflows matter more than everything else, automate tests around those workflows and run them after every substantial AI-generated change. Interface screenshot tests, API regression tests, authentication checks, and critical data-flow tests can catch behavior that nobody remembered to manually inspect.

Version control is equally important. The agent should work through changes that can be reviewed, compared, and reverted rather than turning the project directory into an archaeological dig where nobody knows what happened between working and broken.

An agent that says “I also cleaned up several related areas” should trigger curiosity rather than gratitude.

Related areas have an irritating habit of becoming unrelated incidents.

Why These Problems Keep Reappearing

The common thread is not that AI systems are uniquely bad at security. Most of these problems existed long before anyone started generating applications through natural language, and experienced developers have spent decades finding new ways to create them manually. AI simply changes the economics by making software production much cheaper and much faster.

That means assumptions can now be converted into code almost immediately. An incomplete authorization model can spread across twenty routes before anyone manually reviews the first one, a secret can be copied into several generated files in seconds, and a dependency decision can propagate through an entire project while the agent is still responding to the original request. Mistakes gain implementation speed right alongside features.

The answer is not to abandon AI-assisted development or force every project through a heavyweight software process. The answer is to place verification where the speed creates uncertainty. Define security boundaries before generating features, give the agent narrow tasks, test the application from outside the normal interface, inspect what actually changed, and treat generated functionality as something that still has to earn trust.

That last point is probably the most important.

Working software is the beginning of the security conversation, not the end of it.

A Short Pre-Launch Security Check

A small application does not need an extensive audit before anyone can use it, but several checks provide disproportionate value. Most can be performed quickly, and together they catch a surprising share of the failures that recur in vibe-coded projects.

The goal is not to prove the application has no vulnerabilities, because no short checklist can do that. The goal is to deliberately test the assumptions AI-generated projects most commonly leave unexamined before real users discover those assumptions for you.

  • Search the current repository and its recent history for credentials, tokens, API keys, passwords, and other secrets.
  • Call protected API endpoints directly instead of relying on what the interface allows or hides.
  • Create at least two user accounts and attempt to read, modify, and delete one account's objects while authenticated as the other.
  • Verify that important application failures produce usable logs or alerts without exposing sensitive data.
  • Restore a current backup into an isolated environment and confirm that the restored application actually works.
  • Treat client-supplied prices, roles, ownership values, workflow states, and similar data as untrusted input.
  • Review the complete AI-generated diff and run regression tests against the application's most important workflows.

None of these checks is exotic. That is precisely why they are useful, because the most damaging problems in AI-generated applications are often not exotic either. They are ordinary security failures hiding behind extraordinary development speed.

Build Fast, but Make the Application Prove Itself

There is nothing inherently wrong with producing a working application in a weekend. If AI can collapse several weeks of repetitive development into a couple of days, that is a capability worth using. The mistake is assuming that development time and verification time collapsed by exactly the same amount.

An AI agent can generate five thousand lines of code much faster than someone can understand them. It can build ten endpoints faster than somebody can manually test ten authorization boundaries, and it can wire together three external services before anyone has considered what happens when the second one fails. The implementation bottleneck has shifted, which means security work has to follow.

The good news is that these seven problems are not mysterious. They can be searched for, tested, reproduced, logged, constrained, and prevented using techniques security professionals already understand. Secrets can be isolated, authorization can be enforced server-side, ownership can be tested, failures can be monitored, restores can be practiced, client input can be distrusted, and agent changes can be bounded.

That is a much better position than dealing with an entirely new class of unknowable technology risk.

AI may have changed how quickly software appears, but it did not change the basic rule that has always separated a convincing demo from a dependable system. The application does not get to declare itself secure just because it runs; the agent does not get to declare its own work correct just because the tests it created pass; and the happy path does not get to define the security boundary simply because it looks good during the demonstration.

No comments:

Post a Comment