GitHub Actions run block command injection

Pass untrusted GitHub Actions event values as data instead of inserting them directly into shell code.

Description

Issue and PR titles or bodies, comments, commit messages and author information can be controlled by external users. Inserting these values into a run command through a ${{ }} expression substitutes them into the code before the shell executes it, creating a command-injection risk. Quoting the expression alone is insufficient.

Potential impact

  • Injected commands can access runner files and the tokens or secrets available to the job.
  • Depending on those permissions, an attacker may alter artifacts or misuse repository operations.

Remediation

  • Do not interpolate external values into inline shell code. Pass needed values through the step’s env and use quoted shell variables such as "$VALUE". Do not evaluate the value again with eval or similar mechanisms.
  • Validate expected formats, keep command arguments separate, and restrict GITHUB_TOKEN and secret access to what the job needs.
  • Do not check out or execute untrusted PR code in privileged workflows such as pull_request_target.

Examples

The first example inserts the PR body into a command. The second is a separate example of fixed repository test commands, assuming trusted base-branch code and the required test script.

Before

yaml
name: Pull Request Workflow

on:
  pull_request_target:
    types:
      - opened

jobs:
  process_pull_request:
    runs-on: ubuntu-latest
    steps:
      - name: Echo Pull Request Body
        run: |
          echo "Pull Request Body: ${{ github.event.pull_request.body }}"

The PR body becomes part of the shell code. Putting attacker-controlled text inside double quotes does not remove the injection risk.

After

yaml
name: check-go-coverage

on:
  pull_request_target:
    branches: [master]

jobs:
  coverage:
    name: Check Go coverage
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Source
        uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - name: Set up Go 1.22.x
        uses: actions/setup-go@v5
        with:
          go-version: 1.22.x
      - name: Run test metrics script
        id: testcov
        run: |
          make test-coverage-report | tee test-results
          echo "coverage=$(cat test-results | grep "Total coverage: " test-results | cut -d ":" -f 2 | bc)" >> $GITHUB_ENV

These commands do not directly insert the PR body. If external values are added later, pass them as data and continue to keep untrusted code out of privileged execution.

References