One over-permissioned dispatcher Lambda is all it takes to give a sandboxed AI agent admin-level reach. Here are six IAM controls, with policy examples, that close that path on AWS.
A missing-authorization bug in an AWS setup function is a clear example of how privilege escalation happens through Lambda, and why AI coding agents are likely to recreate the same design.
If an AI agent, or any automation in your AWS account, can invoke a Lambda function that runs with broad permissions, that function is part of the agent's attack surface. Looking at the agent's own IAM role only gives you half the picture. In practice, this comes down to three things: restrict who can invoke setup and dispatcher functions to specific named callers, attach a permissions boundary to any role an agent is able to create, and remove setup functions once setup is done.
On September 22, AWS released a security bulletin for CVE-2026-94384 (CVSS 3.1 base score 8.1), a missing-authorization flaw in sfExecuteAWSService. The function is bundled with AmazonConnectSalesforceLambda, the Serverless Application Repository app that links Amazon Connect with Salesforce. Versions 5.15 through 5.24.16 are affected. The fix, version 5.26, shipped on June 29, nearly three months ahead of public disclosure.
The flaw itself is straightforward. sfExecuteAWSService is intended to run only during initial setup. It accepts parameters from the caller, forwards them to AWS service APIs and executes those calls under its own execution role, without verifying whether the caller should be allowed to make that request.
As a result, any IAM principal with lambda:InvokeFunction on that single function can carry out operations its own identity is explicitly denied, up to whatever the function's execution role permits. The function effectively becomes a proxy for privilege escalation.
Version 5.26 introduces a SalesforceExecuteAWSServiceUser parameter that limits which principal can invoke the function. AWS also advises deleting or disabling sfExecuteAWSService once setup is finished.
This function was designed by a person, not a model. But it's exactly the kind of pattern AI coding agents tend to produce on their own, and at far greater volume.
Ask a coding agent to automate infrastructure setup or wire together an internal workflow, and there's a good chance it ends up with something very close to this design. From the agent's point of view, every step along the way makes sense.
A generic dispatcher is the simplest thing to write. A single handler that accepts a service name, an action and a payload handles every case with minimal code, and it looks clean in review.
When setup fails with AccessDenied, the fastest fix is to broaden the execution role. Figuring out the minimum permissions takes longer, and nothing in the agent's loop rewards it for questioning whether the wider grant is safe.
Then, once the task reports success, the agent moves on. Unless it was told to, it won't remove the setup function or revisit the trust policy it just wrote.
The other half of the problem is what happens when the agent is the caller. A common assumption is that giving the agent a narrow role, such as lambda:InvokeFunction in a sandbox, keeps the blast radius small. This CVE shows why that assumption falls apart.
If an over-privileged function with no input validation is reachable, the agent doesn't need admin rights of its own. All it needs is the function's ARN and a well-formed payload. That can come from prompt injection, a hijacked goal, or a CI job that simply targets the wrong ARN. A single generic dispatcher with a privileged role gives a low-privilege agent everything that role can do.
The examples below use placeholder account IDs and names. Try them in a sandbox account before rolling them out.
Neither agents nor developers should deploy a Lambda that accepts free-form service and action parameters. Here's a simplified version of the pattern behind this CVE:
import boto3
def handler(event, context):
# The caller chooses the service, the API and the arguments
client = boto3.client(event["service"])
return getattr(client, event["action"])(**event["params"])
Anything the execution role can do, any caller can now do too. A safer handler calls one specific operation and validates its input first:
import json
import re
import boto3
s3 = boto3.client("s3")
BUCKET = "app-config-bucket"
KEY_PATTERN = re.compile(r"config/[a-z0-9-]{1,64}\.json")
def handler(event, context):
key = event.get("key", "")
if not KEY_PATTERN.fullmatch(key):
raise ValueError("invalid key")
s3.put_object(Bucket=BUCKET, Key=key, Body=json.dumps(event["config"]))
Write the rule into your agent instructions, then enforce it in CI. A static analysis rule, for example in Semgrep, can flag dynamic calls such as getattr on SDK clients.
An agent can only deploy a function with a privileged role if it's permitted to pass that role. Restrict iam:PassRole to a small set of pre-approved roles, and only for Lambda:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PassOnlyApprovedLambdaRoles",
"Effect": "Allow",
"Action": "iam:PassRole",
"Resource": "arn:aws:iam::111122223333:role/agent-lambda-*",
"Condition": {
"StringEquals": { "iam:PassedToService": "lambda.amazonaws.com" }
}
}
]
}
This stops the problem at deploy time, before a privileged dispatcher ever exists.
In this CVE, the privilege lived in the dispatcher's execution role, not in the caller's role. So a boundary on the agent's own role isn't sufficient. Every role an agent creates for a function needs a boundary as well, and the agent must not be able to remove or rewrite it.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ManageRolesOnlyWithBoundary",
"Effect": "Allow",
"Action": [
"iam:CreateRole",
"iam:PutRolePolicy",
"iam:AttachRolePolicy",
"iam:PutRolePermissionsBoundary"
],
"Resource": "arn:aws:iam::111122223333:role/agent-lambda-*",
"Condition": {
"StringEquals": {
"iam:PermissionsBoundary": "arn:aws:iam::111122223333:policy/agent-boundary"
}
}
},
{
"Sid": "ProtectTheBoundary",
"Effect": "Deny",
"Action": [
"iam:DeleteRolePermissionsBoundary",
"iam:CreatePolicyVersion",
"iam:DeletePolicyVersion",
"iam:SetDefaultPolicyVersion",
"iam:DeletePolicy"
],
"Resource": [
"arn:aws:iam::111122223333:role/*",
"arn:aws:iam::111122223333:policy/agent-boundary"
]
}
]
}
The first statement ensures the agent can only create or modify roles that carry the boundary. The second prevents it from deleting the boundary or editing the boundary policy itself. To catch issues before deployment, IAM Access Analyzer custom policy checks can fail a CI build when a policy grants access you've ruled out.
lambda:InvokeFunction shouldn't be a default developer entitlement. Scoping the function's resource policy isn't enough by itself. Within a single account, a call is allowed if either the caller's identity policy or the function's resource policy allows it, and Lambda resource policies can't deny.
So remove lambda:InvokeFunction on "*" from identity policies. Then, in AWS Organizations, add a Service Control Policy that denies invocation of setup functions to everything except your deployment pipeline:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "OnlyPipelineInvokesSetupFunctions",
"Effect": "Deny",
"Action": "lambda:InvokeFunction",
"Resource": "arn:aws:lambda:*:*:function:setup-*",
"Condition": {
"ArnNotLike": {
"aws:PrincipalArn": "arn:aws:iam::*:role/deploy-pipeline"
}
}
}
]
}
This depends on a naming convention for setup functions. SCPs also don't apply to the management account, so keep workloads out of it.
When an agent creates a bootstrap function to seed a database or configure resources, the deployment script should delete it once the run completes. If a helper needs to stay, restrict invocation to the one principal that requires it, which is what the SalesforceExecuteAWSServiceUser parameter does in 5.26. If it's rarely used, leave it disabled until someone deliberately re-enables it.
If you're running AmazonConnectSalesforceLambda 5.15 through 5.24.16, upgrade to 5.26 or later. Then delete or disable sfExecuteAWSService if setup is complete. The deployed function name usually includes sfExecuteAWSService, so this command finds it in a region:
aws lambda list-functions \
--query "Functions[?contains(FunctionName, 'sfExecuteAWSService')].[FunctionName, LastModified]" \
--output table
Run it in every region where you've deployed the integration. More broadly:
Agents probably won't come up with new attack techniques inside your AWS account. The likelier problem is that they repeat the design mistakes people have been making for years, just faster and in more places. By the time a CVE like this one surfaces, the cleanup has usually ended up on someone else's desk.