Skip to main content
cd ../config-traps
risk/register/lambda-passrole-wildcards-build-their-own-admin-role.html
AWS IAM Privilege Escalationcritical severityAWS IAM

Lambda PassRole Wildcards Build Their Own Admin Role

Overview

A Lambda execution role granting iam:PassRole on Resource: "*" without an iam:PassedToService condition lets compromised function code pass any account role to EC2, ECS, or Glue. Attackers with mere code execution pivot straight to AdministratorAccess-level compute, and CloudTrail logs look like routine service calls.

At a glance

Unsafe setting
iam:PassRole Allow with Resource: "*" and no iam:PassedToService condition on Lambda execution role
Failure trigger
Compromised function code (dependency, injected payload, deserialisation bug) calls PassRole against a privileged role ARN
Blast radius
Attacker launches EC2, ECS, or Glue resources inheriting a passed admin-level role, compromising the entire AWS account.
Recommended control
Scope PassRole to named ARNs with iam:PassedToService condition; enforce via permission boundaries and org-wide SCP denying condition-less PassRole.

Fix commands and configuration

{"Effect": "Allow", "Action": "iam:PassRole", "Resource": "arn:aws:iam::123456789012:role/glue-etl-runtime", "Condition": {"StringEquals": {"iam:PassedToService": "glue.amazonaws.com"}}}
iam:PassRole
iam:PassedToService

The Trap

An IAM policy attached to a Lambda execution role that grants iam:PassRole with Resource: "*" and no iam:PassedToService condition. The function can pass control of any role in the account to any service that accepts a role parameter, regardless of what that role was designed to do.

The Default State

Serverless scaffolds and CDK constructs that orchestrate Step Functions, Glue jobs, or ECS tasks from a Lambda need to pass a role to the downstream service, so the generated policy statement is written once, broadly, and never revisited: {"Effect": "Allow", "Action": "iam:PassRole", "Resource": "*"}. Developers add it to unblock a deploy, the pipeline passes, and the statement sits unreviewed because no security tool flags PassRole the way it flags AdministratorAccess.

The Blast Radius

An attacker who gets code execution inside the function, via a poisoned npm or PyPI dependency, an injected event payload, or a deserialisation bug, does not need to escalate their own IAM identity. They call iam:PassRole against the ARN of your CI/CD deploy role, your CloudFormation execution role, or a role with AdministratorAccess, then launch an EC2 instance, ECS task, or Glue job using that borrowed identity. The new compute resource inherits full permissions of the passed role, and the attacker pivots straight out of a sandboxed function into whatever that role can touch, often the entire account. Because the Lambda’s own execution role never changed, CloudTrail shows a legitimate-looking service call, not a privilege escalation event, so detection lags for days.

The Lead Mechanic Fix

Scope the statement to named role ARNs and pin the destination service with a condition key: {"Effect": "Allow", "Action": "iam:PassRole", "Resource": "arn:aws:iam::123456789012:role/glue-etl-runtime", "Condition": {"StringEquals": {"iam:PassedToService": "glue.amazonaws.com"}}}. Never let a function pass a role more privileged than its own execution role; enforce that with a permission boundary attached to every role the function is allowed to pass. At the organisation level, add an SCP denying iam:PassRole where the iam:PassedToService condition key is absent, closing the wildcard path account-wide regardless of what individual teams write into Terraform.