Securing Agent Access to Third-Party APIs and SaaS Credentials
Agents leak credentials because they hold broader access than tasks require.

Securing agent access to third-party APIs is not the same problem as securing a backend integration, and treating it like one is how credentials end up in the wrong hands. When a backend service calls Salesforce or Slack, an engineer wrote the code path in advance: it hits a known endpoint, with a known credential, in a known order, and someone can audit that path before it ever runs in production. An agent doesn't work that way. It decides at runtime which tool to call, based on whatever text it's reading in a ticket, an email, or a shared doc, and the same prompt can produce a different sequence of tool calls depending on the day.
That difference breaks the OAuth model that SaaS platforms built for deterministic apps, in three specific ways. First, action selection is non-deterministic: run the same prompt twice and the agent might call three tools one time and five the next, in a different order. Second, scopes chain in ways nobody scoped for: read access on contacts plus write access on notes can, in combination, let an agent do something neither permission was meant to allow alone. Third, and this is the one that matters most for security teams, you cannot audit a decision that hasn't been made yet. A cron job queries the same table every night with the same credential. An agent, in a single workflow, might query a database, draft an email, schedule a meeting, and file a support ticket, and each of those four actions needs a different permission against a different system. There is no fixed code path to review, because the code path is decided live, by a model, with a token in its hands.
How agents leak credentials: the failure modes that keep showing up
Two failure classes account for nearly everything that goes wrong. The first is over-privileged access: the agent holds scopes broader than any single task requires, so a wrong tool call doesn't just fail, it becomes a fully valid API request that the provider has no reason to reject. The second is credential leakage: the agent has direct visibility into the token itself, so a prompt injection or a plain model mistake can put that token into an output log, a chat transcript, or a downstream message.
None of the recent incidents required jailbreaking a model. Each one involved an agent using access it had already been granted, correctly, by the letter of its configuration.
In August 2025, the Salesloft Drift breach, attributed to a group tracked as UNC6395, used stolen OAuth tokens to reach customer environments across more than 700 organizations. Once inside, the attacker didn't need to escalate anything clever: it searched Salesforce exports for AWS keys, Snowflake tokens, and VPN credentials sitting in plain text. In June 2025, a logic flaw in the Asana MCP server exposed task data, project metadata, comments, and files across organizations, though the scope of exposure varied by affected account. The API authenticated every request correctly. The MCP layer simply failed to preserve the tenant boundary between customers, which affected roughly 1,000 accounts.
The Composio incident, reported in 2026, shows how far a single stolen token can travel. An attacker used a compromised Gmail OAuth token to intercept magic-link emails, get into an internal monitoring tool, register malicious tool definitions, and then reach a credential cache from inside the execution sandbox itself. That chain ended with 5,001 GitHub OAuth tokens compromised and 5,241 API keys treated as potentially exposed. Around the same period, Wiz researchers found a Supabase API key sitting exposed in the front-end JavaScript of a vibe-coded app; the key granted full read and write access to production data, exposing 1.5 million API authentication tokens, 35,000 email addresses, and private messages. In April 2026, an AI coding agent deleted a Railway production database nine seconds after finding a long-lived API token in an unrelated file; the token had account-wide permissions with no separation between environments, so the agent had every right, technically, to do what it did.
The Supabase MCP case is maybe the cleanest illustration of the whole problem. A support ticket contained instructions telling an agent to read the integration_tokens table and paste the contents into a reply on that same ticket. The database accepted the query without complaint, because the credential attached to it had permission to run it. Nothing was hacked. The system worked exactly as configured, and that was the failure.
Every one of these incidents shares the same root cause: a credential with more access than the task needed, held for longer than the task needed, with no easy way to audit or revoke it mid-flight. And this cause produces a scale larger than any single incident suggests. GitGuardian's State of Secrets Sprawl 2026 report counted nearly 29 million new hardcoded secrets committed to public GitHub repositories in 2025 alone, with AI-assisted commits leaking secrets at roughly double the rate of manually written ones. Inside MCP configuration files specifically, GitGuardian found around 24,000 exposed secrets sitting in public repos.
Why prompt instructions cannot substitute for access controls
The instinct, once a team notices this problem, is almost always to write a better system prompt: "only use the get_contact tool," or "never call anything with delete in the name." It feels like it should work, and it doesn't, because a system prompt shapes what the model is inclined to do, not what the model is capable of doing. If the tool is exposed in the agent's context, the model can call it, regardless of what the instructions say about when it should.
A study (Uppala, arXiv:2605.18414) put a number on how much a prompt-level restriction actually buys you. With tools simply visible in context and no restriction at all, unauthorized tool invocation happened between 48.5% and 68.5% of the time, depending on the model. Adding a prompt-level allowlist, telling the model explicitly which tools it's allowed to touch, drops the failure rate, but only to somewhere between 4.0% and 37.0%. The fix that actually worked was structural: remove the tools from the model's context entirely and verify every call through a proxy sitting outside the model. That brought unauthorized invocation down to 0%, at a median overhead of 1.72 milliseconds.
Sit with that 4% number for a second. That's the best case for prompt-based restriction, the low end of the range, and it still means roughly one in twenty-five interactions calls a tool it shouldn't have access to. A security boundary that fails one time in twenty-five is a coin flip stretched over twenty-five tries, not a boundary. It's a countdown until someone finds the prompt injection that lands in that window.
The MCP attack surface
A standardized protocol for connecting agents to external tools has become the common way agents plug into external tools: it's the adapter that sits between a model and whatever API or service it needs to touch. Adoption moved fast, and security maturity did not keep pace with it. Between January and February 2026, researchers filed more than 30 CVEs against MCP servers, clients, and the infrastructure around them. One of them was a remote code execution flaw rated 9.6 on the CVSS scale, sitting in a package that had already been downloaded close to 500,000 times.
None of the root causes were exotic. Missing input validation, no authentication at all in some cases, and tools that blindly trust whatever description or metadata came bundled with them. Trend Micro found 492 MCP servers running with no client authentication and no traffic encryption whatsoever. OX Security went further and identified architectural design flaws affecting more than 7,000 publicly reachable MCP servers.
The specific gap that matters for credentials is subtle but important: MCP authentication verifies that a client is talking to the right server, but it does nothing to enforce which tenant, which connection, which tool, or which arguments that authenticated agent is actually allowed to use once it's inside. That's exactly the gap that let Asana's MCP layer leak data across tenant boundaries even though the underlying API authenticated every request correctly. It's also how a poisoned tool description, malicious metadata baked into a tool's definition, can trick an agent into misusing a perfectly legitimate tool based on instructions that were never supposed to be there. And it's how the Composio attacker reached a credential cache from inside the execution sandbox after already being inside the system.
The fix has to be architectural, not procedural. An MCP server needs to act as a credential boundary in its own right: the agent holds a scoped OAuth token good for calling the tool, and the actual downstream credential, the one that talks to the real service, gets pulled server-side, inside the MCP backend, somewhere the agent never sees it.
Credential architecture: the identity models and storage patterns that contain blast radius
Before choosing a storage pattern, someone has to answer a more basic question: on whose behalf is the agent acting? Four identity models answer that differently, and each fits a different job. A bot or service identity has the agent acting as itself, which works well for pure automation where every action should be attributable to a bot, not to any particular human. Per-user OAuth has the agent act on behalf of a specific person, which matters anywhere record-level permissions differ by user, the way a Salesforce rep's visibility differs from a colleague's. A shared org identity, where an admin connects once and every user in the company benefits, is the fastest to set up, but it pushes all the enforcement logic onto the application itself, since the credential layer no longer distinguishes between users. Project or workspace-scoped identity fits multi-tenant products where different teams need genuinely separate integration contexts. Most real products end up needing all three at once, requiring per-user OAuth for the CRM, a bot token for Slack notifications, and an API key for internal analytics.
Once the identity model is settled, the actual authentication mechanism matters just as much, and these rank fairly cleanly by security posture. Secretless, cloud-native auth, AWS IAM roles, GCP service accounts, Azure Managed Identities, is the top: the runtime mints short-lived tokens automatically, and there's no static secret sitting anywhere to steal or rotate. That's the right fit for internal cloud workloads where the infrastructure itself is controlled. OAuth 2.1 and OIDC with short-lived tokens is the standard fallback for external SaaS platforms: the agent authenticates as an OAuth client, gets a scoped token, and calls the API on the user's behalf. Long-lived API keys sitting in environment variables are the worst option on the list, and unfortunately the most common one: no expiration, no scope limits, no separation between environments, so the same key that runs a harmless read-only lookup can also overwrite a production table.
Shared service accounts deserve a specific warning. Once multiple agents authenticate through the same account, individual behavior can no longer be attributed to any one of them, because revoking access from a single misbehaving agent means revoking it for every agent riding on that account. HashiCorp Vault, for instance, can generate a fresh credential on demand with a configurable time-to-live, solving a piece of this because the agent never holds anything long-lived. If that credential is compromised, the attacker walks away with a token good for a few hours, not one that works indefinitely.
Enforcing tool boundaries and keeping credentials out of the model context
The pattern that actually holds up in production is simple to state: give the agent a connection identifier, never the provider's access token. The agent calls a tool by name, with arguments and an opaque connection ID attached. A separate tool executor resolves that ID against a credential store, loads the real credential, refreshes it if it's expired, and builds the actual API request. The response comes back to the agent, but the Authorization header itself never touches the model's context window. If the agent gets prompt-injected, the attacker who reads its output learns a connection ID, which is useless outside the system, not a credential that works anywhere else.
Scope enforcement should live at the credential level, not in the instructions. Configure the exact scopes a provider app needs, request only those scopes during the connection flow, and if the agent has both read and write tools available, only expose the tools that the granted scopes actually support. Gmail's gmail.readonly scope is a clean example: a request carrying a read-only token that tries to hit a write endpoint gets denied by Gmail itself, not by the agent choosing to behave. That is the difference between an action being structurally impossible versus merely discouraged.
The Uppala study's headline result, tools removed from context entirely, each call checked by a proxy outside the model, unauthorized invocations at 0%, points to where the real enforcement has to sit. Not in the prompt. In the executor layer that decides what actually reaches the provider. Read-only should be the default at that layer: state-mutating calls need their own, separately credentialed tools, requested explicitly rather than bundled in by default. Zero-storage middleware between the agent and the provider, where no token is ever written to disk in the execution environment, closes off one more place for a credential to sit around waiting to be found.
Human approval gates as an access control, not just an audit mechanism
An approval step is usually framed as a compliance checkbox: something added after the fact so a security team can point to a log and say a human looked at it. That framing undersells what the gate is actually doing. Placed correctly, in front of a state-mutating action rather than after it, a human approval step is an access control in its own right, functioning the same way a scope restriction does: it decides whether an action is allowed to happen at all, not whether it gets written down afterward.
The distinction matters because of where the agent's non-determinism actually lives. A scope restriction can stop an agent from calling a tool it was never meant to touch, but it can't evaluate whether a specific, permitted action is the right one to take in a specific, unusual context, the way flagging an unusually large wire transfer is a judgment call about that instance, not about whether the wire tool should exist. Read operations, low-risk lookups, anything reversible can run without a person in the loop. Anything that mutates state in a way that's expensive or hard to undo, deleting records, sending money, filing something externally, deserves a checkpoint where a person has to actually say yes before the request reaches the provider.
Built that way, the approval gate does double duty. It stops the single worst outcome of an agent's non-determinism, the wrong action executing at the wrong moment, and it produces the audit trail a security team wanted from the compliance checkbox as its original purpose. The two goals aren't in tension. Treated as an access control instead of an afterthought, the human approval step ends up being the cheapest, most legible layer in the entire stack, precisely because it doesn't need a proxy, a scope negotiation, or a new identity model to work. It just needs someone with the authority to say no, standing between the agent's decision and the system that will act on it.


