Build1 publisher2 min readPublished
A Z3 solver found an AWS deny policy covered four of nine compute-launch privesc paths
Bala Paranj ran a 2022 AWS privilege-escalation case through a Z3 solver and found its six-action deny policy blocked four of nine compute-launch vectors. Expanding the list to all nine leaves a residual, because each new compute service AWS ships opens a fresh bypass path.
The Engineer · Build desk

What happened
- Edoardo Rosa's July 2022 writeup showed a principal holding the DataScientist and EMRFullAccess managed policies could reach admin despite an explicit deny policy.
- cloudformation:UpdateStack and lambda:InvokeFunction sit in the deny even though neither launches compute, padding the list without adding coverage.
- Rosa found one missing path by hand, an Auto Scaling launch configuration plus group, and left four other uncovered vectors untouched.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- contradiction Paranj's write-up counts five bypasses in its summary but one reachable vector in its SAT output; the four others are stopped by the PassRole condition, not by the deny the piece is about.
- constraint Because the role could only be passed to ec2.amazonaws.com, constraining which services a role can be passed to closes most of the registry without naming any compute action.
- precedent Each new compute service with a passable role adds a path no hand-written deny list named, so the list ages on AWS's release schedule rather than the team's.
- capability Expressing deny-list review as a solver query makes gap-finding decidable, so coverage no longer depends on whether a reviewer happened to enumerate every service.
In AWS IAM a deny removes actions that an allow granted. The prover reproduces that by walking the principal's three policies, DataScientist, EMRFullAccess, and the DemoDenyPrivEscs deny, and computing the effective set, where an action counts only if it appears in some Allow and in no Deny. [11] It then tests nine compute-launch vectors, each of them a set of actions that, with iam:PassRole, ends at an EC2-like environment running as the role you named. [6] A vector is reachable only when every required action is permitted, iam:PassRole is allowed and not denied, and the target service appears in a PassRole condition. [11]
That last test is why the deny list looks worse on paper than in this specific case. Paranj's summary says the solver "finds five bypasses," but Finding 1 reports one vector reachable out of nine. [18][12] The deny covers none of autoscaling, ECS, CodeBuild, Glue, or SageMaker. [9] Four of those five never fire here, because EMRFullAccess's PassRole condition only lists ec2.amazonaws.com: ECS needs ecs-tasks.amazonaws.com in the condition, CodeBuild needs codebuild.amazonaws.com, and Glue and SageMaker need their own service principals, none of which are present. [13] Autoscaling slips through because an Auto Scaling group launches an EC2 instance, and ec2.amazonaws.com is in the condition. [14]
Four of the five gaps were contained by the PassRole condition, not by the deny list. [13] The open route is autoscaling:CreateLaunchConfiguration plus autoscaling:CreateAutoScalingGroup [3]: the launch configuration names an admin role as its instance profile, the group boots an EC2 with that role, and the principal reads the credentials from IMDS. [4]
Expand the deny to cover all nine vectors and the solver proves what is left. Every new compute service AWS ships is another launch path, so the list goes stale the day a service with a PassRole-capable role lands. [15] Paranj calls the deny-list approach "structurally fragile." [17]
The original list looks like what it probably was. Paranj writes that it reads like the output of a security review in which someone enumerated the ways to launch EC2 with a role and copied them into a policy. [16] That method caught four of the nine vectors [7] and two actions, cloudformation:UpdateStack and lambda:InvokeFunction, that launch no compute at all. [8][1] Rosa, inspecting the service catalog by hand, found one of the five it missed; the other four stayed untouched until a solver with a vector registry walked all nine. [10][5]
What to watch
- Whether AWS ships a compute service with a passable role that the nine-vector registry does not yet include.
- Whether the Z3 check gets packaged so teams can run it against their own IAM policies.
- Whether remediation guidance moves toward constraining PassRole conditions as the primary control.