From 177ef37664d6eca1d530b375daca3ffddbdf1ebc Mon Sep 17 00:00:00 2001 From: Koosha Pari Date: Thu, 2 Jul 2026 00:36:01 -0700 Subject: [PATCH] fix(ci/security): remediate workflow injection + unpinned action MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit quality-gate.yml: bind workflow_call inputs to env vars before shell use to prevent script injection via inputs.not-applicable, inputs.coverage-report, inputs.coverage-format, inputs.threshold. Pattern: env: VARNAME: ${{ inputs.x }} then $VARNAME in run:. latency-budget.yml: pin actions/github-script to full commit SHA (f28e40c7f34bde8b3046d885e986cb6290c5673b, v7) — was unpinned @v7 tag on a workflow with pull-requests:write scope, allowing tag mutation to execute arbitrary code with write permissions. --- .github/workflows/latency-budget.yml | 4 ++-- .github/workflows/reusable/quality-gate.yml | 18 ++++++++++++------ 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/.github/workflows/latency-budget.yml b/.github/workflows/latency-budget.yml index 3ba33daf4956..273d5da61fc4 100644 --- a/.github/workflows/latency-budget.yml +++ b/.github/workflows/latency-budget.yml @@ -74,7 +74,7 @@ jobs: - name: Annotate PR with budget results if: always() - uses: actions/github-script@v7 + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 with: script: | const fs = require('fs'); @@ -141,7 +141,7 @@ jobs: - name: Comment PR with regression report if: always() - uses: actions/github-script@v7 + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 with: script: | const fs = require('fs'); diff --git a/.github/workflows/reusable/quality-gate.yml b/.github/workflows/reusable/quality-gate.yml index 4e197463d47a..882be67b0980 100644 --- a/.github/workflows/reusable/quality-gate.yml +++ b/.github/workflows/reusable/quality-gate.yml @@ -51,12 +51,14 @@ jobs: name: Quality Gate runs-on: ubuntu-latest steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Parse not-applicable gates id: parse-na + # Bind workflow_call input to env var before use in shell — prevents script injection. + env: + NA_GATES: ${{ inputs.not-applicable }} run: | - NA_GATES="${{ inputs.not-applicable }}" echo "na_gates=${NA_GATES}" >> $GITHUB_OUTPUT [[ "$NA_GATES" == *"a11y"* ]] && echo "skip_a11y=true" >> $GITHUB_OUTPUT || echo "skip_a11y=false" >> $GITHUB_OUTPUT [[ "$NA_GATES" == *"e2e"* ]] && echo "skip_e2e=true" >> $GITHUB_OUTPUT || echo "skip_e2e=false" >> $GITHUB_OUTPUT @@ -67,8 +69,10 @@ jobs: - name: Validate coverage report if: ${{ !contains(steps.parse-na.outputs.na_gates, 'coverage') }} + # Bind workflow_call input to env var before use in shell — prevents script injection. + env: + REPORT: ${{ inputs.coverage-report }} run: | - REPORT="${{ inputs.coverage-report }}" if [ ! -f "$REPORT" ]; then echo "❌ Coverage report not found: $REPORT" exit 1 @@ -78,10 +82,12 @@ jobs: - name: Parse coverage threshold if: ${{ !contains(steps.parse-na.outputs.na_gates, 'coverage') }} id: coverage + # Bind workflow_call inputs to env vars before use in shell — prevents script injection. + env: + THRESHOLD: ${{ inputs.threshold }} + FORMAT: ${{ inputs.coverage-format }} + REPORT: ${{ inputs.coverage-report }} run: | - THRESHOLD="${{ inputs.threshold }}" - FORMAT="${{ inputs.coverage-format }}" - REPORT="${{ inputs.coverage-report }}" if [ "$FORMAT" = "lcov" ]; then LINES_HIT=$(grep '^LH:' "$REPORT" | awk -F: '{sum += $2} END {print sum}') LINES_FOUND=$(grep '^LF:' "$REPORT" | awk -F: '{sum += $2} END {print sum}')