ai newsFr., 16. Okt.
EilmeldungHow to track your brand in Google AI Mode

One Prompt Took Over Every Agent in the Account: The Default Role Is the Blast Radius

Zenity Labs showed one prompt to a public agent returning credentials for a role scoped to every agent in the account. The model made one HTTP request. Everything after that was IAM, and IAM is code you can check today. Your public agent answers questions all day. A user asks it to fetch a URL. It

dev.toFreitag 14:46 · 16.10.10 Min. Lesezeit
Bild: dev.to

Zenity Labs showed one prompt to a public agent returning credentials for a role scoped to every agent in the account. The model made one HTTP request. Everything after that was IAM, and IAM is code you can check today.

Your public agent answers questions all day. A user asks it to fetch a URL. It fetches the URL. That is the job you gave it.

Now picture that URL returning the temporary credentials of the machine the agent runs on, and picture those credentials belonging not to that one agent, but to every agent in the same account and region. Your quoting agent just became a key to your finance agent, its conversations, its secrets, and its memory. Nobody jailbroke anything. Nobody found a clever prompt. The request worked because the role said it could.

On October 8, 2026, at the SecTor conference in Toronto, Zenity Labs disclosed a chain it called AgentCorruption in Amazon Bedrock AgentCore, the managed platform AWS offers for deploying and operating AI agents. The research had been reported to AWS starting December 25, 2025. According to the disclosure summary, a single prompt to one public-facing agent could reach the instance metadata service at 169.254.169.254, return the temporary credentials assigned to that workload, and then use a default execution role whose permissions were not scoped to that one agent.

From there, the reported chain moved in directions that should sound familiar to anyone who has reviewed an IAM policy. Discovery across the account. Invocation of other agents in the region. Reading conversations across agents and users. Pulling container images and reading source code. Reading API keys and tokens from a secrets store. Writing memories that persisted after the session ended and redirected later conversations.

AWS moved AgentCore to IMDSv2 for newly deployed agents by February 14, 2026, and the summary reports that the wider default-role permissions were removed by September 29, 2026, 278 days after the first report. AWS described the original behavior as documented and expected, and pointed customers to a practice you have heard for years: grant an execution role only the permissions its workload needs.

You can argue about the timeline. You can argue about what a managed platform should ship by default. Both arguments miss the part you control this afternoon. If you copied a default role, accepted a generated role, or widened a role until the demo stopped failing, your blast radius is a policy document sitting in your own account. You do not need a research team to read it. You need an hour and a refusal to skim.

The blast radius was a role property, not a model property.

That is the gap this piece fills. Most write-ups stop at the chain: prompt, metadata service, credentials, takeover. The useful work starts one step later, when you turn the chain into five concrete checks against your own role policy and run them before your agent meets the public.

An execution role is the set of actions a workload can perform, on the resources named in the policy, for as long as the credentials are valid. An agent holding that role does not need to persuade an authorization system. It presents signed credentials, and the cloud checks the signature and the policy, not the intention behind the request and not whether a human or a model typed the prompt.

That distinction explains the whole incident. A prompt injection is a content problem. It becomes an account problem only when the content can reach a tool, the tool can reach credentials, and the credentials can reach resources the agent has no business touching. Break any one of those three links and the chain ends as an embarrassing chat transcript. Leave all three in place and a customer service agent is a lateral movement tool.

Notice what the dangerous permissions have in common. None of them looks dramatic on its own:

Each line is a normal permission with an abnormal scope. That is why reviews miss them. A reviewer scanning action names sees familiar verbs: invoke, list, get, create. The damage lives in the resource field, in the star where a specific identifier should be, and stars are easy to scroll past at the end of a long policy.

Do not start by reading your policy and nodding. Start by asking it questions, the same questions an attacker would ask with your credentials in hand. The script below encodes two policies for the same public quoting agent. The first mirrors the pattern in the disclosure: useful actions, all scoped to a wildcard. The second is the same agent rewritten to itself only: its own secret prefix, its own memory, its own log group, and no permission to invoke, read, or enumerate any other agent at all.

import fnmatch BROAD = { " Statement ": [ { " Effect ": " Allow ", " Action ": [ " bedrock-agentcore:InvokeAgentRuntime " ], " Resource ": " * " }, { " Effect ": " Allow ", " Action ": [ " bedrock-agentcore:ListEvents " ], " Resource ": " * " }, { " Effect ": " Allow ", " Action ": [ " bedrock-agentcore:GetResourceApiKey ", " secretsmanager:GetSecretValue " ], " Resource ": " * " }, { " Effect ": " Allow ", " Action ": [ " bedrock-agentcore:CreateEvent " ], " Resource ": " * " }, { " Effect ": " Allow ", " Action ": [ " logs:DescribeLogGroups " ], " Resource ": " * " }, ] } OWN_SECRET = " arn:aws:secretsmanager:us-east-1:111122223333:secret:quoting/* " OWN_MEMORY = " arn:aws:bedrock-agentcore:us-east-1:111122223333:memory/quoting-memory " SCOPED = { " Statement ": [ { " Effect ": " Allow ", " Action ": [ " secretsmanager:GetSecretValue " ], " Resource ": OWN_SECRET }, { " Effect ": " Allow ", " Action ": [ " bedrock-agentcore:CreateEvent " ], " Resource ": OWN_MEMORY }, { " Effect ": " Allow ", " Action ": [ " logs:CreateLogStream ", " logs:PutLogEvents " ], " Resource ": " arn:aws:logs:us-east-1:111122223333:log-group:/agents/quoting:* " }, ] } FINANCE = " arn:aws:bedrock-agentcore:us-east-1:111122223333:runtime/finance-agent " FINANCE_SECRET = " arn:aws:secretsmanager:us-east-1:111122223333:secret:finance/payroll " FINANCE_MEMORY = " arn:aws:bedrock-agentcore:us-east-1:111122223333:memory/finance-memory " def allows ( policy, action, resource ): for st in policy [ " Statement " ]: actions = st [ " Action " ] if isinstance ( st [ " Action " ], list ) else [ st [ " Action " ]] resources = st [ " Resource " ] if isinstance ( st [ " Resource " ], list ) else [ st [ " Resource " ]] if any ( fnmatch. fnmatchcase ( action, a ) for a in actions ) and \ any ( fnmatch. fnmatchcase ( resource, r ) for r in resources ): return True return False CHECKS = [ ( " invoke the finance agent ", " bedrock-agentcore:InvokeAgentRuntime ", FINANCE ), ( " read conversations on the finance agent ", " bedrock-agentcore:ListEvents ", FINANCE ), ( " read the finance payroll secret ", " secretsmanager:GetSecretValue ", FINANCE_SECRET ), ( " plant a memory on the finance agent ", " bedrock-agentcore:CreateEvent ", FINANCE_MEMORY ), ( " enumerate agents via log groups ", " logs:DescribeLogGroups ", " * " ), ( " read its OWN quoting secret (legit work) ", " secretsmanager:GetSecretValue ", " arn:aws:secretsmanager:us-east-1:111122223333:secret:quoting/catalog-key " ), ( " write its OWN memory (legit work) ", " bedrock-agentcore:CreateEvent ", OWN_MEMORY ), ] for name, policy in [( " BROAD default role ", BROAD ), ( " SCOPED role ", SCOPED )]: print ( f " \n { name } " ) allowed = 0 for label, action, resource in CHECKS: ok = allows ( policy, action, resource ) allowed += ok print ( f " { ' ALLOW ' if ok else ' DENY ' } { label } " ) print ( f " -> { allowed } / { len ( CHECKS ) } requests allowed " ) Enter fullscreen mode Exit fullscreen mode Run it with a plain interpreter, no packages to install:

python3 check_role.py Enter fullscreen mode Exit fullscreen mode On my run, the broad role allowed 7 of 7 requests. The scoped role allowed 2 of 7, and the two it allowed were the agent's own legitimate work: reading its own catalog secret and writing its own memory. Every cross-agent request flipped to deny, and nothing the agent actually needs broke. That last point matters, because the usual objection to scoping is that the agent will stop working. Here it does not. The work survives. The takeover does not.

A few things are worth noting about this example. First, the evaluator is deliberately small: wildcard matching on action and resource, which is close enough to a first-pass policy review to catch the failure mode in the disclosure. Second, the test set is the product. Writing the seven checks forced the design question a policy read never does: should a quoting agent ever invoke a finance agent, in any circumstance, for any user? If the answer is no, the policy should say no, not merely hope the prompt does. Third, keep the two legitimate checks in the suite forever. A scoping change that passes the attack checks and fails the work checks will get reverted at the next incident, usually by someone widening the role back to a star at 2 a.m.

Adapt the checks to your platform in this order:

[public agent] --prompt--> [tool: fetch URL] --reaches--> [metadata service] | v [other agents] <--invoke-- [credentials of the DEFAULT ROLE] --read--> [secrets] ^ | | v [future sessions] <--planted-- [memory write across agents] <--read-- [conversations] Enter fullscreen mode Exit fullscreen mode Read the diagram as a checklist, not a picture. Can a tool your agent holds reach a metadata address? Should outbound fetch be limited to an allowlist of hosts, with link-local addresses denied outright? If credentials leak anyway, and you should design as if they will, which of the five arrows still works? A good scoped role leaves the attacker holding valid credentials to almost nothing.

The script tests a policy you paste into it. Your account needs the same treatment against the role that is actually attached. Pull it, do not rely on the console summary you remember:

aws iam get-role --role-name YOUR_AGENT_ROLE aws iam list-attached-role-policies --role-name YOUR_AGENT_ROLE aws iam get-role-policy --role-name YOUR_AGENT_ROLE --policy-name YOUR_POLICY_NAME aws iam simulate-principal-policy \ --policy-source-arn arn:aws:iam::111122223333:role/YOUR_AGENT_ROLE \ --action-names bedrock-agentcore:InvokeAgentRuntime secretsmanager:GetSecretValue \ --resource-arns arn:aws:bedrock-agentcore:us-east-1:111122223333:runtime/finance-agent Enter fullscreen mode Exit fullscreen mode The simulation call is the one teams skip, and it is the one that answers the only question that matters: given this exact role, this exact action, and that exact other agent, does the cloud say yes? Run it for each of the five dangerous pairs above, once per sensitive agent in the account. Record the answers in the pull request that scopes the role.

A reviewer can argue with an opinion about least privilege. A simulation table with five allows on cross-agent actions is harder to wave away.

While you are there, treat memory as part of the compromised state, not as harmless context. If a role could write memory across agents or users, your incident plan needs a step that inspects and clears memories written outside their own session, after credentials rotate. Rotating the key and restarting the agent leaves a planted memory exactly where the attacker put it, ready for the next user who trusts the agent's name.

The minimal viable version fits in an afternoon. Export the role policy. Write the seven checks for your most public agent and your most sensitive agent. Run them. Replace every star on a dangerous action with a named resource, split any shared secret the checks expose, and add the test file to CI so a future widening fails the build instead of shipping quietly.

You will be tempted to spend next week on prompt defenses, because prompts feel like the new part and IAM feels like homework. Resist that ordering. A prompt defense reduces how often the agent tries something it should not. A scoped role decides whether trying works. One of those is a probability. The other is a boundary.

Export one production agent role today, run the five cross-agent checks against it, and fix the first star you find before you touch another system prompt.

When you pulled your agent's actual role policy for the first time, which permission surprised you most, and who had added it?

Quelle: dev.to (dev.to). Original öffnen