Lambda CreateFunction Plus PassRole: The Escalation Combo
A deployment role with iam:PassRole scoped to Resource: * but no iam:PassedToService condition lets any principal with lambda:CreateFunction attach an administrator role to a brand-new function, then harvest its temporary credentials on invocation, bypassing every IAM boundary the console appears to enforce.
At a glance
- Unsafe setting
- iam:PassRole granted with Resource: * and no iam:PassedToService condition on a Lambda deployment role.
- Failure trigger
- A principal with lambda:CreateFunction attaches an existing administrator role to a new function and invokes it to extract temporary credentials.
- Blast radius
- Any CI or developer identity with PassRole and CreateFunction can self-escalate to any role in the account without exploiting Lambda code itself.
- Recommended control
- Scope PassRole to specific execution-role ARNs with an iam:PassedToService condition restricted to lambda.amazonaws.com.
The Trap
An IAM policy grants iam:PassRole with Resource: * to a CI/CD deployment role or a developer group, intended only to let pipelines pass a fixed set of Lambda execution roles during deploys. No condition key restricts which service the role can be passed to, and no PassRole resource ARN pattern limits which roles qualify.
The Default State
Terraform modules and Serverless Framework IAM templates commonly generate a deploy role with “Action”: “iam:PassRole”, “Resource”: “*” because scoping PassRole to specific execution-role ARNs breaks every time a new function is added, and nobody wants to edit the policy on each release. AWS’s own SAM quickstart examples ship this pattern by default, and it passes every automated linter that only checks for Resource: * on iam:*, not the PassRole-plus-CreateFunction combination specifically.
The Blast Radius
Any identity holding iam:PassRole (unscoped) alongside lambda:CreateFunction or lambda:UpdateFunctionCode can attach an existing highly-privileged role — including an administrator role used elsewhere in the account — to a new or modified function, then invoke it. The function code simply calls sts:GetCallerIdentity or reads its own execution environment to exfiltrate temporary credentials scoped to that attached role. This is a documented escalation primitive (Rhino Security Labs catalogued it under Lambda:CreateFunction + IAM:PassRole), and it requires no exploitation of Lambda itself — only the two IAM permissions most CI users already hold. CloudTrail logs CreateFunction and Invoke as expected activity, so detection tooling tuned for anomalous IAM API calls sees nothing unusual until the stolen credentials are used elsewhere.
The Lead Mechanic Fix
Scope iam:PassRole to explicit execution-role ARNs and add a condition requiring iam:PassedToService equals lambda.amazonaws.com: {“Effect”:”Allow”,”Action”:”iam:PassRole”,”Resource”:”arn:aws:iam::ACCOUNT_ID:role/lambda-exec-*”,”Condition”:{“StringEquals”:{“iam:PassedToService”:”lambda.amazonaws.com”}}}. Separately deny lambda:CreateFunction and lambda:UpdateFunctionConfiguration from attaching any role outside that ARN prefix using a policy condition on lambda:ExecutionRoleArn. Run IAM Access Analyzer’s policy validation check for PassRole-without-condition as a pre-merge CI gate, and remove Resource: * from every deployment role that predates this control.