aws-cfn-update-stack-role-swap-modifies-config
cloudformation:UpdateStack changing only the stack's service role (--role-arn) to a more-privileged role: swaps the execution identity without modifying the template.
match
A conjunctive graph pattern. Variables (?x) bind node ids; every clause must hold.
where
BuildWorker # AWS::CloudFormation::Stack
?stack has RoleARN set (current service role exists)
?new_role is more privileged than the stack's current RoleARN OR ?new_role is a different privileged role
iam:PassRole for ?new_role permits iam:PassedToService == cloudformation.amazonaws.com (or unconditioned)
emit
| source type | Identity |
|---|---|
| target type | Compute |
| source | ?principal |
| target | ?stack |
| permissions | cloudformation:UpdateStack iam:PassRole |
| conditions | iam_permission role_compatibility service_state |
| state logic | ACTIVE when (a) ?principal has EFFECTIVE cloudformation:UpdateStack on ?stack, (b) iam:PassRole for ?new_role is ACTIVE with iam:PassedToService including cloudformation.amazonaws.com or unconditioned, AND (c) the stack is mutable (CREATE_COMPLETE, UPDATE_COMPLETE, UPDATE_ROLLBACK_COMPLETE). CONDITIONAL(role_compatibility) when ?new_role trust policy does not yet include cloudformation.amazonaws.com — UpdateStack will fail because CloudFormation cannot assume the role. BLOCKED if SCP or permission boundary denies cloudformation:UpdateStack or iam:PassRole at the effective scope. |
Narrative
{principal.name} can call cloudformation:UpdateStack on stack {stack.name} to change its service role (--role-arn) and has iam:PassRole for {new_role.name} (iam:PassedToService == cloudformation.amazonaws.com). By updating the stack with only a new RoleARN and no template change, the attacker rebinds the stack to execute as {new_role.name} without CloudFormation modification logic interfering. On the next stack operation, resource API calls run as {new_role.name}. can-execute-as propagates this to CanExecuteAs({principal.name} -> {new_role.name}).
Raw rule rules/derived/aws/cloudformation.yaml
id: aws-cfn-update-stack-role-swap-modifies-config
emits: CanModifyConfiguration
description: 'cloudformation:UpdateStack changing only the stack''s service role (--role-arn) to a more-privileged
role: swaps the execution identity without modifying the template.'
match:
- - principal: null
- CanPassIdentity
- new_role: null
where:
- ?principal has EFFECTIVE cloudformation:UpdateStack on ?stack
- 'node_type(?stack) == BuildWorker # AWS::CloudFormation::Stack'
- ?stack has RoleARN set (current service role exists)
- ?new_role is more privileged than the stack's current RoleARN OR ?new_role is a different privileged
role
- iam:PassRole for ?new_role permits iam:PassedToService == cloudformation.amazonaws.com (or unconditioned)
emit:
source_type: Identity
target_type: Compute
source: ?principal
target: ?stack
permissions:
- cloudformation:UpdateStack
- iam:PassRole
conditions:
- iam_permission
- role_compatibility
- service_state
state_logic: "ACTIVE when (a) ?principal has EFFECTIVE cloudformation:UpdateStack on ?stack, (b) iam:PassRole\
\ for ?new_role is ACTIVE with iam:PassedToService including cloudformation.amazonaws.com or unconditioned,\
\ AND (c) the stack is mutable (CREATE_COMPLETE, UPDATE_COMPLETE, UPDATE_ROLLBACK_COMPLETE). CONDITIONAL(role_compatibility)\
\ when ?new_role trust policy does not yet include cloudformation.amazonaws.com \u2014 UpdateStack\
\ will fail because CloudFormation cannot assume the role. BLOCKED if SCP or permission boundary denies\
\ cloudformation:UpdateStack or iam:PassRole at the effective scope."
confidence: 0.92
derived_from:
- ?principal CanPassIdentity ?new_role
- cloudformation:UpdateStack EFFECTIVE on ?stack
- ?stack RoleARN is set
false_positive_note: "CanModifyConfiguration alone (without CanPassIdentity to a more-privileged role)\
\ is not escalation \u2014 swapping to a less-privileged or sideways role is not an attack. Only emit\
\ when ?new_role is provably more privileged or when the principal can demonstrate arbitrary role\
\ choice. Honor iam:PassedToService: a role passable only to services other than cloudformation.amazonaws.com\
\ does NOT qualify. can-execute-as.yaml execute-as-via-config-identity-swap rolls CanModifyConfiguration\
\ + CanPassIdentity into CanExecuteAs \u2014 cite, do not re-derive CanExecuteAs here."
narrative: '{principal.name} can call cloudformation:UpdateStack on stack {stack.name} to change its
service role (--role-arn) and has iam:PassRole for {new_role.name} (iam:PassedToService == cloudformation.amazonaws.com).
By updating the stack with only a new RoleARN and no template change, the attacker rebinds the stack
to execute as {new_role.name} without CloudFormation modification logic interfering. On the next stack
operation, resource API calls run as {new_role.name}. can-execute-as propagates this to CanExecuteAs({principal.name}
-> {new_role.name}).'