Blog

Security Drift: What happens when you let different AI agents build your APIs


Fahad Faisal Cyber Security Researcher Intern

The use of APIs has been increased with the introduction of AI agents and MCP servers. Instead of manually developing every API applications, the developers now use various coding assistants or agents to build an entire codebase easily and faster.

The problem is that during this fast development process using the agents, security controls are often overlooked or not implemented unless they are clearly stated. If the required security context is not provided correctly to the every agents individually, each development cycle may weakens the application's security posture little by little. As a result more vulnerabilities gets introduced into the code because security is not treated as a core requirement from the start.

Cross-Agent Security Drift

Developers often rely on multiple AI agent tools for developing the API applications. Teams switch between different coding agents for different tasks. Developers do this because each agent has different strengths. A typical workflow looks like this:

A typical multi-agent development workflow, with different coding assistants touching the same repository over time

As a result, the codebase may be modified by several independent agents throughout its development cycle. If the security related context is not provided to the each agents separately, it may follow the existing patterns from the code, which can leads to vulnerabilities. This is not just specific to vibe-coded API applications only, every LLM-generated part of the application gets affected by this problem. But the API bugs have more severity, because the authentication and authorization logic is depended on the API. The problem is when different agents treat the security context of the API codebase differently. It is not a problem with any coding agents itself. It is the developer's duty to provide these agents with enough context to each agents about the actual output needed.

Agentic Rule Files

Repository-level instruction files help agents understand project conventions, coding standards, and security requirements that may not be in the API specification alone. An endpoint definition may describe what an API should do, but it often lacks information about which users are allowed to use it. Guidelines, including security measures, can be documented through different files depending on which agent the developer uses (e.g., AGENTS.md, CLAUDE.md, etc.). There is currently no universal standard across different agents defining which files are prioritized, or how conflicting instructions are resolved.

Let's say a codebase's initial version is generated using a specific agent and later the developer change to use any other agents for other tasks depending on the capability of the agent. Each agents may have different context about the codebase. During this development cycle, if the developer forgets to add security instructions for the agents in every run, the agents may prioritize the patterns observed in the code and follow that direction. It is the developer's responsibility to give the each agents about the important information in the development phase rather than leaving the agent to develop blindly. Without giving the proper knowledge about what the developer actually wants, the agents will develop the security measures on its own which can leads to unintended behaviours.

1. Initial build using claude code

We start by creating an API application using Claude Code, which follows repository instructions from CLAUDE.md and produces routes correctly aligned with security measures from the instruction file. In our CLAUDE.md file, we stated that every write endpoint requires requireAdmin().

The CLAUDE.md rule requiring requireAdmin() on every write endpoint
Figure 1 - The requireAdmin() rule in CLAUDE.md.

Claude Code created the fully working endpoints with every write endpoint having requireAdmin(), and the full codebase looks properly secured and every endpoint it generated has the admin-only check.

Claude Code generated routes, each write endpoint guarded by requireAdmin()
Figure 2 - Claude Code routes, all following the rule.

Drift checkpoint: every write endpoint requires admin exactly as CLAUDE.md says. Nothing has broken yet.

Now we add some user-specific endpoints that can do read or write actions, and those do not need requireAdmin() because they are actions on the user's individual profile, like editing their name or updating their profile picture.

User-specific endpoints that intentionally do not use requireAdmin()
Figure 3 - Self-service endpoints that legitimately skip requireAdmin().

2. Feature development using Cursor

In the next phase, we continue working on the same codebase but instead of using Claude Code, we switch to another coding assistant (e.g., "Cursor") and build an invitation feature for managing invitations across the organization. This may get tricky for agents deciding what permission model needs to be implemented on the invitation routes. Depending on the permission model, there are two ways: an admin or a normal member in the organization can do this inviting action. But the context about the permissions is not given in the prompt. We give only a minimal prompt describing the functionality without talking about the security measures needed here, like:

Prompting Cursor to implement an invitations feature without mentioning authentication or authorization
Figure 4 - The Cursor prompt, describing features only.

We also didn't move the security controls mentioned in CLAUDE.md over to a context file that Cursor actually prioritizes. Cursor reads CLAUDE.md, but it prioritizes AGENTS.md and its own Project Rules more (cursor's docs on rules). Our prompt never mentions authentication or authorization, assuming it was already covered in CLAUDE.md. The agent creates fully working code as requested. But here, instead of using the authorization model for endpoints defined in CLAUDE.md, it looks through the whole codebase and sees there are also user-specific endpoints that don't have requireAdmin() (their actions are tied to the user profile only and don't do any write actions to the organization). So it goes ahead and generates this code with a separate middleware called requireOrgMember(). The endpoints work as expected, but the API's original permission model has been changed.

Cursor generating an invitations endpoint protected by requireOrgMember instead of requireAdmin
Figure 5 - Cursor using requireOrgMember() instead.

After Cursor generated the endpoint, we asked it why it chose requireOrgMember() even though the repository instructions in CLAUDE.md clearly state that every write endpoint should use requireAdmin().

Cursor explaining why it chose requireOrgMember instead of requireAdmin
Figure 6 - Cursor explaining that it copied the surrounding code.

Cursor agreed that the implementation violated the documented policy. Its explanation shows that it developed a permission model from surrounding code instead of treating CLAUDE.md as the primary source and assumed that inviting users was a member-level operation because similar patterns already existed elsewhere in the repository.

The endpoint behaves exactly as requested. Users can invite other users, invitations are stored correctly. But in this phase, the agent didn't get much context about the permission model from us like in the initial phase, so the agent assumed that an organization member can initiate these invitations and generated the code for doing that. The permission model got changed and this change violated the critical rule we set in CLAUDE.md: "Every write endpoint requires requireAdmin()." Now a feature that was originally intended to be admin-only has become available to every organization member.

Drift checkpoint: invitations went from admin-only to any org member silently and nothing in the pipeline flagged it. In the next phase this drift continues.

3. Quick Inline edits using Copilot

After building the core of the API application using Claude Code with detailed security context and later using Cursor for some other endpoint generation, we now use GitHub Copilot for some inline edits. The inline edits also don't have context about the security model. Its only knowledge is about the currently opened file and the surrounding code blocks. In our case, that's invitation.ts, the code generated in the second phase by Cursor with the new requireOrgMember(). Using Copilot's autocompletion feature we type a comment and the code gets autocompleted. Copilot does not have any context of the security model or about requireAdmin(), so it completes the code with requireOrgMember() again because the surrounding blocks only contain endpoints with requireOrgMember().

GitHub Copilot autocompleting a new route with requireOrgMember, following the surrounding code
Figure 7 - Copilot autocompleting requireOrgMember() again.

Now this trend has been normalized and all the endpoints are created using this same pattern. The agents are only following what they are supposed to do.

Drift checkpoint: requireOrgMember() is no longer a one-off mistake, it is now the pattern the codebase itself teaches to every agent that reads it next.

4. Fixing the bugs with Codex

With the full API codebase generated, we observed some minor errors in the functionality. Some inconsistencies with the endpoints when different values are passed by users. These are simple edits that can be fixed easily, so we go ahead and prompt Codex to fix that and also look for any other bugs in the codebase (not about security).

Prompting Codex to fix functionality bugs, with no mention of security
Figure 8 - The Codex prompt, about bugs only.

Codex catches some bugs from the invitation functionality and fixes them correctly. But it didn't flag the endpoints with requireOrgMember() created during phases 2 and 3 because our prompts only mentioned functionality related bugs, nothing related to security.

Codex fixing the functional bugs while leaving the requireOrgMember drift untouched
Figure 9 - Codex fixing bugs and leaving the drift untouched.

Drift checkpoint: After two development phases, the requireAdmin() rule written in CLAUDE.md still has not been enforced on this feature, and every tool along the way did exactly what it was asked to do.

5. Reviewing the code with Antigravity

After the whole codebase gets generated across all these phases and agents, we now run a security analysis using Antigravity. But instead of giving it a generic review prompt, we give it more context on what it's actually supposed to look for in the API application:

A security review prompt for Antigravity that points it at the documented rules
Figure 10 - The Antigravity prompt, pointing it at the documented rules.

With this prompt, Antigravity checked the documented rules first, and it caught the exact issue that we were tracking. That Rule 4, every write endpoint needs requireAdmin() was not enforced across 13 endpoints in the codebase.

Antigravity reporting that Rule 4 was not enforced across 13 endpoints
Figure 11 - Antigravity finding Rule 4 broken across 13 endpoints.

It also found some real bugs that happened during these same drift. All of this goes back to the same permission drift from the earlier stages. The agent found that any user could join any organization with no invite check and anyone could accept or decline an invitation that was not even sent to them. The invitations list was showing every pending invite in the system, not just the ones meant for that user.

Now compare that to a normal, generic prompt with only less context about the security actions needed to be checked, the kind most people would actually run:

Review this API codebase for security issues and code quality. Looks for middleware inconsistencies. Flag anything that looks risky.

We ran this exact prompt on the same codebase, and it missed the requireAdmin() drift completely. It only shows up when the agent is told to pull the actual documented rules and compare them against the real code, that's only when the issue shows up correctly.

This is what we refer to as Cross-Agent Security Drift. The generated code by these agents in every phase is not incorrect in functionality. But when we switched agents, due to the lack of security context from our side, the new agents prioritized authorization patterns from the existing code over the instructions already recorded in the previous agent's file.

As we have seen above, if security measures are not properly included in the prompt, the agent prioritizes satisfying the requested functionality rather than prioritizing the permission models. In each cycle, the agent's focus on delivering the code for the functionality faster leads to security issues like these authorization issues.

In vibe coding, the lack of context about security context like "for whom" the generated API is intended is a major contributor to many authorization vulnerabilities, even if the code is protected against issues like SQL injection or XSS. This creates a false sense of security, where developers believe the API still follows the documented authorization model simply because the rules exist in the agent instruction files. One of the reasons most of the AI-generated endpoint issues go unnoticed is that they work properly as requested by the developer, but their behavior is never checked against questions like "Who else can use this endpoint?" or "What should this user actually be limited to?"

Case Study

The BodySnatcher (CVE-2025-12420) exploit was not created through AI-assisted development, but it shows an important lesson for developers increasingly relying on coding assistants to build APIs. None of the individual weaknesses were particularly complex. This is the exact type of risk that can develop during iterative AI-assisted development, even when the API's functionality is correct. The attack was a result several API security measures failed at different layers of the application. In this case, multiple small issues are exploited to achieve full admin takeover of Virtual Agent instances.

Virtual Agent is ServiceNow's enterprise chatbot, and it can be exposed through an API. It maps user inputs using Natural Language Understanding (NLU) to detect what they want and direct them to predefined topics. The exploit chain is a multi-step process that allowed the attacker to become the administrator.

The BodySnatcher exploit chain against ServiceNow's Virtual Agent API
  1. The Virtual Agent API was originally used to let external platforms (Slack, Teams) communicate with ServiceNow's engine. Message Auth is the authentication method the Virtual Agent API relies on and it also uses a static token for making authenticated requests. When ServiceNow extended this for agent-to-agent communication, the new API was built as a wrapper on top of the older one, inheriting its authentication scheme. That token was hardcoded as the literal string servicenowexternalagent and shipped identically across every customer environment. Anyone who knew this string could authenticate as a trusted external provider without a valid ServiceNow account.
  2. Next the application needs to figure out which external user made the incoming requests. A feature called Auto Linking is used across the platform for this usecase. When an email address is provided in the request payload, it links the email to an internal account without any password or MFA. If an administrative email is provided, it maps that session to the administrator.
  3. To perform actions on the application, an agent UID is needed. ServiceNow ships with some default agents across all instances, and these agents share the same UID, just like they share the secrets. An example is the Record Management AI Agent, which can create or modify records in any table. The attacker also need the targeted user role UID, in this case the UID of the admin user which is also the same across instances. The attacker invokes it through an internal topic with a prompt telling it to create a new admin-role user.
  4. The Record Management Agent normally runs in "supervised mode" meaning that a request to make a user an administrator also requires confirmation from the end user. The attacker can provide that confirmation by sending a "please continue" message because it was created by the attacker itself, after which the agent confirms and performs the action.
  5. This action creates a new account and attaches the administrator role to that account, and the password for that account can be obtained through the traditional password reset flow. After logging in with the reset password, full administrative access to the instance is granted.

Defending AI-Generated API endpoints

Instruction files are useful for communicating security requirements across different coding agents, but they should not be the only mechanism used to enforce them. Authorization requirements should also be expressed as executable checks, such as authorization tests, policy-as-code, or CI validation rules. Unlike natural language instructions, executable policies can automatically detect when an AI-generated change unintentionally weakens the API's intended security model.

Here is what that looks like in practice, a test that reads the actual route files and checks them against the documented rule directly, and this will trigger if any of the permission model gets changes during any of the phases.

An authorization test that reads the route files and checks them against the documented rule
Figure 12 - The test that turns the rule into an executable check.

This test reads the actual route files through the TypeScript compiler API, so it keeps catching drift as new routes get added by the agents during the development process. The exemption list in code is the place where the developer can add which routes are allowed to skip admin instead of letting the agents to decide itself. Adding this test to the CI will block the pull request with the branch protection turned on main requiring this check to pass.

To see it work, we went back to the phase where the invitations.ts file is generated by the Cursor, the phase that generated the requireOrgMember() where the initial drift started in the codebase. If an authorization test like this was already there at that point, the CI run on that commit would have caught the drift right at the initial stage, instead of it quietly going forward through Copilot and Codex

The CI run failing on the commit that introduced requireOrgMember
Figure 13 - The test failing on the phase 2 commit where drift began.

Conclusion

Developers need to make application security requirements clear throughout the API's development process. Authentication, authorization, rate limiting, input validation, and other security controls should be treated as core API requirements alongside functional correctness.

  • Give the agent the full context of the API, including its purpose, the different user roles, and which endpoints each role is allowed to access. Clearly document security requirements and authorization requirements so every agent understands which of the API's middleware, permission checks, and security controls always need to be preserved, this reduces the chances of a future cycle unintentionally weakening the API's security model.

And rather than leaving the requirements in a single file like CLAUDE.md, use a global requirement file such as "AGENTS.md" as it will be supported across every agent and prioritized equally. Also this will be very useful for managing the context because the developer needs to only change a single file even when building with multiple agents. If a setup like this was placed here in our development, the agents would have caught the issue more easily and faster. The instruction files being there is only half of it, how you actually ask the agent to use it matters just as much, and this is exactly what we saw in the Antigravity example. A generic "review this for security issues" prompt does not tell the agent to go check what's documented, so it just does not, and the drift stayed invisible. The prompt needs to explicitly tell the agent to find the documented rules first and use them as the source of truth, otherwise the agent is just reviewing the code against its own judgment of what looks reasonable, and in our case, the working API code always looked reasonable.

A well documented rules file is not enough on its own, even a good one has limits, and there is research backing that up. An study byETH Zurich, tested repository-level context files across multiple agents and found that agents don't ignore these files at all. LLM-generated context files actually reduced task success rates compared to having no context file at all, and even well written human ones only gave a small improvement. The files also get more token-expensive and less reliable as they grow, and stale information inside them actively misleads the agent instead of just being ignored.

The global security requirement is the first line of defense. If it's set up correctly, developers don't need to repeatedly specify security measures for every run, which helps prevent security bugs from being introduced. If something still slips through, the second line of defense is the executable checks (such as authorization tests), which prevent the generated API code from reaching production.