From c0208475b3aa6f719454c85287e0a22edc9aeb6c Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Mon, 26 Oct 2026 20:48:46 -0700 Subject: [PATCH] workflows: harden GitHub Actions YAML against injection Interpolating GitHub Actions expressions directly into shell `run:` blocks, `actions/github-script` scripts, and `.bazelrc` configs risks command, script, or flag injection when inputs or refs contain shell metacharacters, newlines, or leading hyphens. Pass workflow inputs and context values through step-scoped `env:` variables, sanitize branch names written to `.bazelrc`, use `printf '%s\n'` instead of `echo`, and guard CLI arguments with `--flag=value` or `--` separators. Also require `parse_comment` to succeed before posting failure reactions on issue comments. --- .github/actions/setup-bazel/action.yaml | 21 ++++++++++++++----- .../workflows/backport_create_releases.yaml | 2 +- .github/workflows/on_comment.yaml | 2 +- .github/workflows/on_pr_closed.yaml | 5 +++-- .github/workflows/pr-metadata-checks.yaml | 14 +++++++++---- .github/workflows/publish_pypi.yaml | 3 ++- .github/workflows/release_add_backports.yaml | 5 +++-- .../workflows/release_build_artifacts.yaml | 10 +++++++-- .github/workflows/release_create_rc.yaml | 2 +- .../release_create_release_branch.yaml | 3 ++- .../workflows/release_process_backports.yaml | 2 +- .github/workflows/release_promote.yaml | 6 +++--- 12 files changed, 51 insertions(+), 24 deletions(-) diff --git a/.github/actions/setup-bazel/action.yaml b/.github/actions/setup-bazel/action.yaml index c6fe1a5691..c5c735cf13 100644 --- a/.github/actions/setup-bazel/action.yaml +++ b/.github/actions/setup-bazel/action.yaml @@ -14,6 +14,14 @@ inputs: runs: using: "composite" steps: + - name: Sanitize branch name + id: sanitize_branch + shell: bash + env: + RAW_BRANCH_NAME: ${{ github.head_ref || github.ref_name }} + # ${var//[^...]/} strips characters outside the safe ref set [a-zA-Z0-9._/-]. + run: printf 'branch_name=%s\n' "${RAW_BRANCH_NAME//[^a-zA-Z0-9._\/-]/}" >> "$GITHUB_OUTPUT" + - name: Setup Bazel uses: bazel-contrib/setup-bazel@0.19.1 with: @@ -40,7 +48,7 @@ runs: build --build_metadata=ACTOR=${{ github.actor }} build --build_metadata=COMMIT_SHA=${{ github.event.pull_request.head.sha || github.sha }} ${{ github.event.pull_request.number && format('build --build_metadata=PR_NUMBER={0}', github.event.pull_request.number) || '' }} - ${{ (github.head_ref || github.ref_name) && format('build --build_metadata=BRANCH_NAME={0}', github.head_ref || github.ref_name) || '' }} + build --build_metadata=BRANCH_NAME=${{ steps.sanitize_branch.outputs.branch_name }} build --build_metadata="BUILDBUDDY_LINKS=[GHA Run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})" # setup-bazel configures .bazelrc and restores caches. Download Bazelisk @@ -52,16 +60,19 @@ runs: shell: bash run: "${GITHUB_ACTION_PATH}/download_bazel.py" - # Use github-script to write the key directly to user.bazelrc without - # passing it via environment variables or CLI args, keeping it out of - # process tables and child environments. + # Use github-script with step-scoped env vars to write the key directly to + # user.bazelrc without passing it via CLI args, job-wide environment + # variables, or JS template interpolations. - name: Write secret to user.bazelrc if: ${{ inputs.buildbuddy_api_key != '' }} uses: actions/github-script@v9 + env: + BUILDBUDDY_API_KEY: ${{ inputs.buildbuddy_api_key }} + BUILDBUDDY_CONFIG: ${{ inputs.buildbuddy_config }} with: script: | const fs = require('fs'); fs.appendFileSync( 'user.bazelrc', - `common:${{ inputs.buildbuddy_config }} --remote_header=x-buildbuddy-api-key=${{ inputs.buildbuddy_api_key }}\n`, + `common:${process.env.BUILDBUDDY_CONFIG} --remote_header=x-buildbuddy-api-key=${process.env.BUILDBUDDY_API_KEY}\n`, ); diff --git a/.github/workflows/backport_create_releases.yaml b/.github/workflows/backport_create_releases.yaml index d6766251bb..b97911a3d1 100644 --- a/.github/workflows/backport_create_releases.yaml +++ b/.github/workflows/backport_create_releases.yaml @@ -44,4 +44,4 @@ jobs: bazel run \ --//python/config_settings:py_pbs_distribution=install_only_stripped \ //dev/release -- \ - backport-create-releases --issue=$ISSUE --no-dry-run + backport-create-releases "--issue=$ISSUE" --no-dry-run diff --git a/.github/workflows/on_comment.yaml b/.github/workflows/on_comment.yaml index 293e389963..49504163af 100644 --- a/.github/workflows/on_comment.yaml +++ b/.github/workflows/on_comment.yaml @@ -151,7 +151,7 @@ jobs: - call_promote - call_backport_prepare - call_backport_create_releases - if: always() && contains(needs.*.result, 'failure') + if: always() && needs.parse_comment.result == 'success' && contains(needs.*.result, 'failure') runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 diff --git a/.github/workflows/on_pr_closed.yaml b/.github/workflows/on_pr_closed.yaml index f6f4b2b0aa..932a876522 100644 --- a/.github/workflows/on_pr_closed.yaml +++ b/.github/workflows/on_pr_closed.yaml @@ -64,9 +64,10 @@ jobs: bazel run \ --//python/config_settings:py_pbs_distribution=install_only_stripped \ //dev/release -- on-pr-merged \ - "$PR_NUMBER" \ --remote origin \ - --no-dry-run + --no-dry-run \ + -- \ + "$PR_NUMBER" call_complete_sync_changelog: needs: parse_pr diff --git a/.github/workflows/pr-metadata-checks.yaml b/.github/workflows/pr-metadata-checks.yaml index 8060ff921e..67b3b77649 100644 --- a/.github/workflows/pr-metadata-checks.yaml +++ b/.github/workflows/pr-metadata-checks.yaml @@ -26,7 +26,8 @@ jobs: PR_BODY: ${{ github.event.pull_request.body }} run: | echo "Checking PR description..." - if echo "$PR_BODY" | grep -Ei "DO NOT SUBMIT|DO NOT MERGE"; then + if printf '%s\n' "$PR_BODY" \ + | grep --extended-regexp --ignore-case --quiet "DO NOT SUBMIT|DO NOT MERGE"; then echo "::error::This PR description contains" \ "'DO NOT SUBMIT' or 'DO NOT MERGE'" \ "and cannot be merged." @@ -43,18 +44,23 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} PR_NUMBER: ${{ github.event.pull_request.number }} + GH_REPO: ${{ github.repository }} run: | echo "Checking for blocked files..." # We use --repo to avoid needing actions/checkout. # This keeps the job lightweight and avoids "not a git repository" errors. - changed_files=$(gh pr diff "$PR_NUMBER" --name-only --repo "${{ github.repository }}") - blocked_files=$(echo "$changed_files" | grep -E '^.agents/(plans|scratch)(/|$)' || true) + changed_files=$(gh pr diff --name-only --repo "$GH_REPO" -- "$PR_NUMBER") + # grep exits 1 when no lines match, which is expected when no blocked files exist. + blocked_files=$( + printf '%s\n' "$changed_files" \ + | grep --extended-regexp '^.agents/(plans|scratch)(/|$)' || true + ) if [ -n "$blocked_files" ]; then echo "::error::Files in .agents/plans and" \ ".agents/scratch are permitted in PRs to" \ "facilitate discussion, but are not allowed" \ "to be merged. Please remove them before merging:" - echo "$blocked_files" + printf '%s\n' "$blocked_files" exit 1 fi echo "No blocked files found." diff --git a/.github/workflows/publish_pypi.yaml b/.github/workflows/publish_pypi.yaml index 345009325f..6273c526ff 100644 --- a/.github/workflows/publish_pypi.yaml +++ b/.github/workflows/publish_pypi.yaml @@ -26,9 +26,10 @@ jobs: - name: Setup Bazel uses: ./.github/actions/setup-bazel - env: + TAG_NAME: ${{ inputs.tag_name || github.ref_name }} # This special value tells pypi that the user identity is supplied within the token TWINE_USERNAME: __token__ # Note, the PYPI_API_TOKEN is for the rules-python pypi user, added by @rickylev on # https://github.com/bazel-contrib/rules_python/settings/secrets/actions TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} - run: bazel run --stamp --embed_label=${{ inputs.tag_name || github.ref_name }} //python/runfiles:wheel.publish + run: bazel run --stamp "--embed_label=$TAG_NAME" //python/runfiles:wheel.publish diff --git a/.github/workflows/release_add_backports.yaml b/.github/workflows/release_add_backports.yaml index db28a03642..3dac0a4720 100644 --- a/.github/workflows/release_add_backports.yaml +++ b/.github/workflows/release_add_backports.yaml @@ -55,5 +55,6 @@ jobs: bazel run \ --//python/config_settings:py_pbs_distribution=install_only_stripped \ //dev/release -- add-backports \ - "${pr_array[@]}" \ - "${ARGS[@]}" + "${ARGS[@]}" \ + -- \ + "${pr_array[@]}" diff --git a/.github/workflows/release_build_artifacts.yaml b/.github/workflows/release_build_artifacts.yaml index 073c863237..bbab6c6d85 100644 --- a/.github/workflows/release_build_artifacts.yaml +++ b/.github/workflows/release_build_artifacts.yaml @@ -128,9 +128,13 @@ jobs: || '' }} - name: Build runfiles wheel + env: + INPUT_REF: ${{ inputs.ref }} + # ${INPUT_REF:+...} expands to the --stamp and --embed_label flags only + # when INPUT_REF is non-empty (release builds, not pull_request builds). run: >- bazel build - ${{ inputs.ref && format('--stamp --embed_label={0}', inputs.ref) || '' }} + ${INPUT_REF:+--stamp "--embed_label=$INPUT_REF"} //python/runfiles:wheel.dist //python/runfiles:wheel.publish //tools/publish:twine @@ -194,7 +198,9 @@ jobs: if-no-files-found: error - name: Create release archive and notes - run: .github/workflows/create_archive_and_notes.sh ${{ inputs.ref }} + env: + INPUT_REF: ${{ inputs.ref }} + run: .github/workflows/create_archive_and_notes.sh "$INPUT_REF" - name: Upload release archive and notes uses: actions/upload-artifact@v7 diff --git a/.github/workflows/release_create_rc.yaml b/.github/workflows/release_create_rc.yaml index 1c6f91738b..fc3ac53d5c 100644 --- a/.github/workflows/release_create_rc.yaml +++ b/.github/workflows/release_create_rc.yaml @@ -59,7 +59,7 @@ jobs: bazel run \ --//python/config_settings:py_pbs_distribution=install_only_stripped \ //dev/release -- \ - create-rc --issue "$ISSUE" --remote origin "${ARGS[@]}" + create-rc "--issue=$ISSUE" --remote origin "${ARGS[@]}" call_release: needs: tag_rc diff --git a/.github/workflows/release_create_release_branch.yaml b/.github/workflows/release_create_release_branch.yaml index dac263e04a..f7db11aa99 100644 --- a/.github/workflows/release_create_release_branch.yaml +++ b/.github/workflows/release_create_release_branch.yaml @@ -38,11 +38,12 @@ jobs: - name: Attempt Branch Creation env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ISSUE: ${{ inputs.issue }} run: | bazel run \ --//python/config_settings:py_pbs_distribution=install_only_stripped \ //dev/release -- \ - create-release-branch --issue "${{ inputs.issue }}" --remote origin + create-release-branch "--issue=$ISSUE" --remote origin # A no-op job that always runs to prevent "no jobs ran" failures # when the main job is skipped. diff --git a/.github/workflows/release_process_backports.yaml b/.github/workflows/release_process_backports.yaml index 483bffdb78..60c6a43af0 100644 --- a/.github/workflows/release_process_backports.yaml +++ b/.github/workflows/release_process_backports.yaml @@ -71,7 +71,7 @@ jobs: bazel run \ --//python/config_settings:py_pbs_distribution=install_only_stripped \ //dev/release -- process-backports \ - --issue "$ISSUE" \ + "--issue=$ISSUE" \ --remote origin \ --no-dry-run \ "${ARGS[@]}" diff --git a/.github/workflows/release_promote.yaml b/.github/workflows/release_promote.yaml index 79b50951ef..a65eb698b2 100644 --- a/.github/workflows/release_promote.yaml +++ b/.github/workflows/release_promote.yaml @@ -49,14 +49,14 @@ jobs: ISSUE: ${{ inputs.issue }} run: | ARGS=() - if [ -n "$VERSION" ]; then - ARGS+=("$VERSION") - fi if [ -n "$ISSUE" ]; then ARGS+=("--issue=$ISSUE") fi ARGS+=("--remote=origin") ARGS+=("--no-dry-run") + if [ -n "$VERSION" ]; then + ARGS+=("--" "$VERSION") + fi bazel run \ --//python/config_settings:py_pbs_distribution=install_only_stripped \