From 24273bf86b2e6d3beb96c7bcbf65fe033fae2aab Mon Sep 17 00:00:00 2001 From: Avelino <31996+avelino@users.noreply.github.com> Date: Mon, 2 Mar 2026 11:51:11 -0300 Subject: [PATCH] Fix pwn request vulnerability in PR quality checks workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The pull_request_target workflow checked out and executed Go scripts from the PR head, allowing attackers to inject arbitrary code via init() functions with access to a write-scoped GITHUB_TOKEN. This was confirmed exploited in the wild (ref: StepSecurity blog). Checkout now targets the base branch so only trusted scripts execute. PR head SHA is fetched as data-only for diffing via a new PR_HEAD_SHA env var. Write operations (comments, labels) are isolated in a separate report job that never checks out code. All job permissions follow least privilege — quality runs read-only, report holds the write token. fixed: #6083 Signed-off-by: Avelino <31996+avelino@users.noreply.github.com> Co-Authored-By: Thierry Abalea --- .github/scripts/check-pr-diff/main.go | 25 +++++++++--- .github/workflows/pr-quality-check.yaml | 53 +++++++++++++++++++------ .gitignore | 1 + 3 files changed, 62 insertions(+), 17 deletions(-) diff --git a/.github/scripts/check-pr-diff/main.go b/.github/scripts/check-pr-diff/main.go index 0479e14b..414a1a24 100644 --- a/.github/scripts/check-pr-diff/main.go +++ b/.github/scripts/check-pr-diff/main.go @@ -207,11 +207,15 @@ func getDiff() string { if base == "" { base = "main" } - out, err := exec.Command("git", "diff", "origin/"+base+"...HEAD", "--", "README.md").Output() + head := os.Getenv("PR_HEAD_SHA") + if head == "" { + head = "HEAD" + } + out, err := exec.Command("git", "diff", "origin/"+base+"..."+head, "--", "README.md").Output() if err == nil && len(out) > 0 { return string(out) } - out, err = exec.Command("git", "diff", "HEAD~1", "--", "README.md").Output() + out, err = exec.Command("git", "diff", head+"~1", "--", "README.md").Output() if err == nil { return string(out) } @@ -223,11 +227,15 @@ func getChangedFiles() []string { if base == "" { base = "main" } - out, err := exec.Command("git", "diff", "--name-only", "origin/"+base+"...HEAD").Output() + head := os.Getenv("PR_HEAD_SHA") + if head == "" { + head = "HEAD" + } + out, err := exec.Command("git", "diff", "--name-only", "origin/"+base+"..."+head).Output() if err == nil && len(out) > 0 { return splitLines(string(out)) } - out, err = exec.Command("git", "diff", "--name-only", "HEAD~1").Output() + out, err = exec.Command("git", "diff", "--name-only", head+"~1").Output() if err == nil { return splitLines(string(out)) } @@ -291,7 +299,14 @@ func extractRepoName(rawURL string) string { // --- README parsing --- func getCategoryItemCount(readmePath, entryURL string) (category string, count int) { - data, err := os.ReadFile(readmePath) + var data []byte + var err error + head := os.Getenv("PR_HEAD_SHA") + if head != "" { + data, err = exec.Command("git", "show", head+":README.md").Output() + } else { + data, err = os.ReadFile(readmePath) + } if err != nil { return "unknown", -1 } diff --git a/.github/workflows/pr-quality-check.yaml b/.github/workflows/pr-quality-check.yaml index 6c69c15d..1a7392ed 100644 --- a/.github/workflows/pr-quality-check.yaml +++ b/.github/workflows/pr-quality-check.yaml @@ -5,8 +5,8 @@ on: types: [opened, edited, synchronize, reopened] permissions: - pull-requests: write - contents: write + contents: read + pull-requests: read jobs: detect: @@ -36,16 +36,30 @@ jobs: runs-on: ubuntu-latest environment: action container: golang:latest + permissions: + contents: read + pull-requests: read + outputs: + comment: ${{ steps.quality.outputs.comment }} + labels: ${{ steps.quality.outputs.labels }} + fail: ${{ steps.quality.outputs.fail }} + diff_comment: ${{ steps.diff.outputs.diff_comment }} + diff_fail: ${{ steps.diff.outputs.diff_fail }} steps: - uses: actions/checkout@v6 with: - ref: ${{ github.event.pull_request.head.sha }} + ref: ${{ github.event.pull_request.base.sha }} + persist-credentials: false fetch-depth: 0 - - name: Fetch base branch + - name: Fetch base branch and PR head + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | git config --global --add safe.directory "$GITHUB_WORKSPACE" - git fetch origin ${{ github.base_ref }} + AUTH="$(printf '%s' "x-access-token:${GITHUB_TOKEN}" | base64 -w0)" + git -c "http.https://github.com/.extraheader=AUTHORIZATION: basic ${AUTH}" fetch origin "${{ github.base_ref }}" + git -c "http.https://github.com/.extraheader=AUTHORIZATION: basic ${AUTH}" fetch origin "+refs/pull/${{ github.event.pull_request.number }}/head" - name: Run quality checks id: quality @@ -59,28 +73,38 @@ jobs: continue-on-error: true env: GITHUB_BASE_REF: ${{ github.base_ref }} + PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} run: go run ./.github/scripts/check-pr-diff/ + report: + name: Post quality report + needs: [detect, quality] + if: always() && needs.detect.outputs.is_package_pr == 'true' && needs.quality.result != 'cancelled' + runs-on: ubuntu-latest + permissions: + pull-requests: write + contents: write + steps: - name: Post quality report comment uses: marocchino/sticky-pull-request-comment@v2 with: header: pr-quality-check message: | - ${{ steps.quality.outputs.comment }} + ${{ needs.quality.outputs.comment }} --- - ${{ steps.diff.outputs.diff_comment }} + ${{ needs.quality.outputs.diff_comment }} - name: Sync labels + if: needs.quality.outputs.labels != '' uses: actions-ecosystem/action-add-labels@v1 - if: ${{ steps.quality.outputs.labels != '' }} with: github_token: ${{ secrets.GITHUB_TOKEN }} - labels: ${{ join(fromJson(steps.quality.outputs.labels), '\n') }} + labels: ${{ join(fromJson(needs.quality.outputs.labels), '\n') }} - name: Fail if critical checks failed - if: ${{ steps.quality.outputs.fail == 'true' || steps.diff.outputs.diff_fail == 'true' }} + if: needs.quality.outputs.fail == 'true' || needs.quality.outputs.diff_fail == 'true' run: | echo "Quality or diff checks failed." exit 1 @@ -90,6 +114,8 @@ jobs: needs: detect if: needs.detect.outputs.is_package_pr == 'false' runs-on: ubuntu-latest + permissions: + pull-requests: write steps: - name: Post skip notice uses: marocchino/sticky-pull-request-comment@v2 @@ -104,9 +130,12 @@ jobs: auto-merge: name: Enable auto-merge - needs: quality - if: always() && needs.quality.result == 'success' + needs: [quality, report] + if: always() && needs.quality.result == 'success' && needs.report.result == 'success' runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write steps: - name: Enable auto-merge via squash env: diff --git a/.gitignore b/.gitignore index 5d92202b..815c6b1b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ out/ awesome-go .cache/ +check-* # Folders .idea