Zenity Labs published AgentCorruption yesterday, alongside a talk at SecTor in Toronto. The short version: one prompt to one public-facing agent on Amazon Bedrock AgentCore, and the researchers could list every AgentCore agent in the same account and region, pull their container images, invoke them, read their users’ private conversations, plant instructions in their long-term memory, and pull secrets out of Secrets Manager.
AWS’s response, as quoted by The Next Web, is that the research “inaccurately paints expected and documented behavior as a vulnerability.”
I think both sides are right, and that’s the part worth your time. Nothing in this chain is exotic. Every link is a default, and every default has been documented somewhere. That’s precisely why it generalises to whatever platform you run agents on.
The chain, link by link
The technical write-up is unusually clear, so here it is as a kill chain, with the control I’d want at each link.
| # | What happened | The default that allowed it | The control that breaks it |
|---|---|---|---|
| 1 | A prompt asked the agent’s built-in http_request tool to fetch http://169.254.169.254/latest/meta-data/iam/security-credentials/ | The microVM didn’t restrict traffic to the metadata service | Deny link-local and metadata addresses on the tool’s network path |
| 2 | The agent returned the execution role’s temporary credentials; the researchers used them from their own machine | Credentials worked from anywhere | Bind credentials to their origin where your platform supports it; short lifetimes regardless |
| 3 | With that role they found agents, pulled images, invoked agents, read conversations | The default role covered all AgentCore resources in the account’s region | One role per agent, resources pinned to that agent’s ARNs |
| 4 | user-data exposed the image URI; images held source and sometimes secrets | Images treated as build output, not as sensitive data | No secrets in images; repository policy scoped per agent |
| 5 | A forged conversation event was stored in memory as a lasting instruction | Memory writes allowed for any holder of the role | Memory writes as a privileged path, with provenance and audit |
| 6 | Tool API keys and Secrets Manager values, including credentials for services outside AWS | Region-wide GetSecretValue | Secrets scoped per agent; assume the blast radius leaves the cloud account |
Zenity says the same result came through the shell tool running curl, which matters for the next section.
Two links deserve more than a table row.
Link 3 is the multiplier. Without it, this is one agent leaking its own credentials, which is bad but bounded. According to Zenity, the default execution role granted actions like bedrock-agentcore:InvokeAgentRuntime, bedrock-agentcore:ListEvents, bedrock-agentcore:CreateEvent and secretsmanager:GetSecretValue at region scope, not agent scope. That turns “an agent was tricked” into “every agent in the region is compromised,” and it’s the difference between an incident and a breach.
Link 5 is the one that should change how you think about agent memory. The planted memory told the agent to visit a researcher-controlled page before every answer. After that, the researchers could change the agent’s behaviour by editing a web page — no further memory writes, no further access to AWS. The persistence moved outside the environment entirely. You could rotate every credential involved and the agent would still be taking instructions from a stranger. I wrote about the five kinds of agent memory as a design choice. This is the threat model that goes with it: memory is a write path into future behaviour, and it needs the same scrutiny as a deploy pipeline.
Why IMDSv2 isn’t the fix
According to both AWS and Zenity, newly deployed AgentCore agents have run IMDSv2-only since 14 February 2026. That’s good. It’s also easy to over-read.
IMDSv2 was designed against open WAFs, open reverse proxies and SSRF bugs — situations where an attacker controls a URL that something else fetches, but can’t choose the HTTP method or add headers. Getting a token needs a PUT with a specific header, and a URL-only SSRF can’t produce one.
An agent’s HTTP tool isn’t a URL-only SSRF. The Strands http_request tool used in the research takes the HTTP method as a free-form parameter — its own schema description lists PUT — and takes arbitrary headers too. A shell tool can run any curl you like. Once the attacker is steering a complete HTTP client in natural language, the PUT is one more sentence in the prompt.
I haven’t tested that path against current AgentCore deployments, and I’m not claiming it works there today. The point is about which layer you’re relying on. IMDSv2 defends against a constrained attacker. Agent tools remove the constraint.
There’s a structural problem underneath. AWS’s own statement is that “AgentCore agents can access the credentials for their own execution role through the metadata service” — the agent’s SDK needs that endpoint. So you can’t just block it for the whole runtime. What you can do is make sure the code path that executes model-chosen network requests is not the code path that can reach the credential endpoint. Route tool traffic through an egress proxy, or run tools in a separate sandbox with no metadata route. The agent’s own SDK keeps its credentials; the tool that fetches arbitrary URLs never sees them.
Do the check at connection time, after DNS resolution and after following redirects. A URL denylist that inspects the string 169.254.169.254 loses to a hostname that resolves there, or a public URL that redirects there.
Both sides are right, which is the problem
AWS’s position, in full: an agent can reach its own role’s credentials by design, cross-account access requires explicit grants on both sides, and customers should grant execution roles only the permissions their agents need. All true. Least privilege is the customer’s job under the shared responsibility model.
Zenity’s position: the default role wasn’t least privilege. By their timeline, they reported the metadata access in December 2025 and the broad role in January 2026; AWS closed the first report as “informative” in April; the role was unchanged when they checked in June; and on 29 September they found it substantially narrowed.
Here’s my problem with “documented and expected.” Most agents in production were deployed by someone following a quickstart, under time pressure, who accepted the default role because the agent worked. For most deployments, the default is the policy. A permission set that’s documented as broad and expected to be narrowed is, in practice, a permission set that ships broad.
It’s the same lesson as the 47th approval click, seen from the vendor’s side: a control that depends on someone doing the careful thing every time decays into whatever the easy path produces, regardless of what the documentation recommends.
What I’d do this week, on any platform
None of this is specific to AgentCore. Swap in your agent runtime of choice.
Count your SSRF surface. List every tool, on every agent, that can make a network request: HTTP, shell, browser, code interpreter, “fetch URL,” anything that takes a URL or a hostname. That list is your SSRF surface. Most teams have never written it down, and it’s usually longer than they think.
Deny metadata and link-local at the network layer for the tool path. On AWS that’s 169.254.169.254 and the IPv6 endpoint fd00:ec2::254. Google Cloud and Azure also serve metadata from 169.254.169.254. Deny the whole 169.254.0.0/16 range and your private ranges unless a tool genuinely needs them, enforce it somewhere the agent process can’t configure, and check after resolution and redirects.
One execution role per agent. Resources pinned to that agent’s ARNs, nothing at region scope. Then go and look at the role your existing agents actually have. Neither AWS’s statement nor Zenity’s write-up says whether agents deployed before the changes picked up the narrower default, so don’t assume yours did.
Treat memory writes as privileged. Record which principal wrote each memory record and from which session. Alert on memory writes that don’t come from the agent’s own conversation path. A memory record that tells an agent to fetch instructions from a URL should be a detection rule in its own right.
Separate customer-facing and internal agents by account. Zenity’s closing advice cautions against running them side by side. An account boundary is the blast-radius boundary that still holds when the role is wrong — and the role is eventually wrong.
The old bug in the new client
SSRF used to need a bug: a URL parameter nobody validated, an image proxy that fetched anything. Agents with general-purpose HTTP tools make SSRF a feature. The request goes wherever the conversation leads, and the conversation is attacker-influenced by construction.
That’s the same shape as yesterday’s OWASP roundup: a boundary that existed as an assumption, met by a component that improvises. The fix is the same too. Don’t rely on the agent declining to make the request. Make sure the request goes nowhere when it does.
Related: Nine Agentic Exploits, One Sentence Worth Memorising · Your agents need identities, not API keys · Secrets Management: Scope, Lifetime, and the Blast Radius of a Leak
Comments