Coordinated Disclosure Timeline

Summary

The SonarCloud analysis workflow copies an external pull request’s attacker-controlled head branch name into a step output and directly interpolates that output into an inline Bash script. An external pull request author can use shell command-substitution syntax in the branch name to execute arbitrary commands in the base repository’s workflow_run job. A later step on the same runner receives the repository’s SONAR_TOKEN, making that credential and the integrity of the SonarCloud analysis reachable from the injected code.

Project

kptdev/kpt repository

Tested Version

The default branch at commit 6dfff203a6b49396add815a428c1d94370e9a3d6.

Details

Pull request branch name injection enables code execution in a privileged SonarCloud workflow (GHSL-2026-207)

The Go workflow runs for pull requests. After the build and test jobs complete, it stores the pull request number in a fixed PR_NUMBER artifact.

Successful completion of that workflow triggers the default-branch SonarCloud analysis workflow. The sonarqube job downloads PR_NUMBER, reads it, and requests the corresponding pull request from the GitHub API. It then extracts .head.ref, which is controlled by the external pull request author through the source branch name, into the pr_meta.outputs.head_ref step output:

- name: Request GitHub API for PR data
  if: github.event.workflow_run.event == 'pull_request' && hashFiles('PR_NUMBER.txt') != ''
  uses: octokit/request-action@b91aabaa861c777dcdb14e2387e30eddf04619ae # v3.0.0
  id: get_pr_data
  with:
    route: GET /repos/{full_name}/pulls/{number}
    number: ${{ steps.pr_number.outputs.content }}
    full_name: ${{ github.event.repository.full_name }}
  env:
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Extract PR metadata
  if: github.event.workflow_run.event == 'pull_request' && hashFiles('PR_NUMBER.txt') != ''
  id: pr_meta
  env:
    PR_DATA: ${{ steps.get_pr_data.outputs.data }}
  run: |
    echo "number=$(echo "$PR_DATA" | jq -r '.number')" >> "$GITHUB_OUTPUT"
    echo "head_ref=$(echo "$PR_DATA" | jq -r '.head.ref')" >> "$GITHUB_OUTPUT"
    echo "base_ref=$(echo "$PR_DATA" | jq -r '.base.ref')" >> "$GITHUB_OUTPUT"

Using PR_DATA through an environment variable protects the extraction step itself. The subsequent debug step, however, directly interpolates the resulting output into Bash source:

- name: Debug - Show PR metadata
  if: github.event.workflow_run.event == 'pull_request'
  run: |
    echo "=== DEBUG: PR Metadata ==="
    echo "PR Number: ${{ steps.pr_meta.outputs.number }}"
    echo "Head Ref: ${{ steps.pr_meta.outputs.head_ref }}"
    echo "Base Ref: ${{ steps.pr_meta.outputs.base_ref }}"
    echo "=== END DEBUG ==="

GitHub Actions resolves ${{ ... }} expressions before passing the generated script to Bash. Double quotes do not neutralize shell command substitution.

The successful-run and coverage-artifact conditions restrict when the vulnerable path executes, but they do not authorize the pull request author who controls the branch name. A representative attack sequence is:

  1. An external contributor creates a fork branch whose name contains command-substitution syntax and opens a pull request that exercises the Go workflow.
  2. The pull request workflow completes successfully and uploads its coverage and PR_NUMBER artifacts. A repository policy may require a maintainer to approve the fork workflow run first.
  3. The workflow_run workflow retrieves the pull request and propagates its attacker-controlled .head.ref through pr_meta.outputs.head_ref.
  4. The debug step inserts the value into the generated Bash script, executing the embedded command on the sonarqube runner.
  5. The injected process can alter runner or workspace state before the later SonarQube scan step receives SONAR_TOKEN.

Impact

An external fork pull request author can execute arbitrary shell commands in a base-repository workflow_run job. Although the job’s GITHUB_TOKEN permissions are read-only, a later action in the same job is explicitly given the repository secret SONAR_TOKEN. Injected code can persist influence over the runner or modify files used by later steps, allowing it to capture or misuse that credential when the SonarQube action starts. This can disclose the token and compromise the integrity of SonarCloud analysis for kptdev_kpt.

The attacker requires no push access to kptdev/kpt: control of an external pull request’s source branch name is sufficient. Exploitation also requires the Go workflow to complete successfully, the expected artifacts to exist, and SONAR_TOKEN to be configured.

CWEs

Resources

Credit

This issue was discovered and reported by GHSL team member @JarLob (Jaroslav Lobačevski).

Contact

You can contact the GHSL team at securitylab@github.com, please include a reference to GHSL-2026-207 in any communication regarding this issue.