From 161da56585e1c8ebe2ceb1a7ec65b28f8f4414ad Mon Sep 17 00:00:00 2001 From: Johannes Millan Date: Thu, 19 Mar 2026 12:26:43 +0100 Subject: [PATCH] fix(ci): close pull_request_target pwn request vulnerability (#6879) Split PR preview into two workflows using the workflow_run pattern: - pr-preview-build.yml (pull_request): builds without secret access - pr-preview-deploy.yml (workflow_run): deploys pre-built artifact with secrets Prevents fork PR authors from executing arbitrary code with access to CLOUDFLARE_API_TOKEN, UNSPLASH_KEY, and other repository secrets. --- .github/workflows/pr-preview-build.yml | 48 ++++++++++++++++ .github/workflows/pr-preview-deploy.yml | 76 +++++++++++++++---------- 2 files changed, 93 insertions(+), 31 deletions(-) create mode 100644 .github/workflows/pr-preview-build.yml diff --git a/.github/workflows/pr-preview-build.yml b/.github/workflows/pr-preview-build.yml new file mode 100644 index 0000000000..6b22e4736e --- /dev/null +++ b/.github/workflows/pr-preview-build.yml @@ -0,0 +1,48 @@ +name: PR Preview Build + +on: + pull_request: + types: [opened, synchronize, reopened] + paths-ignore: + - 'docs/wiki/**' + +permissions: + contents: read + +jobs: + build-preview: + runs-on: ubuntu-latest + + steps: + - name: Check out Git repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Setup Node.js + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + with: + node-version: 22 + cache: 'npm' + + # work around for npm installs from git+https://github.com/johannesjo/J2M.git + - name: Reconfigure git to use HTTP authentication + run: > + git config --global url."https://github.com/".insteadOf + ssh://git@github.com/ + + - name: Install npm Packages + run: npm i + + - name: Generate environment file + run: npm run env + + - name: Build Frontend for Web + run: npm run buildFrontend:prodWeb + + - name: Upload build artifact + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + with: + name: pr-preview-build + path: dist/browser + retention-days: 1 diff --git a/.github/workflows/pr-preview-deploy.yml b/.github/workflows/pr-preview-deploy.yml index 215e0d34e4..4eb8110ffd 100644 --- a/.github/workflows/pr-preview-deploy.yml +++ b/.github/workflows/pr-preview-deploy.yml @@ -1,50 +1,64 @@ name: PR Preview Deploy on: - pull_request_target: - types: [opened, synchronize, reopened] - paths-ignore: - - 'docs/wiki/**' + workflow_run: + workflows: ['PR Preview Build'] + types: [completed] + permissions: + actions: read contents: read pull-requests: write deployments: write jobs: deploy-preview: + if: > + github.event.workflow_run.event == 'pull_request' && + github.event.workflow_run.conclusion == 'success' continue-on-error: true runs-on: ubuntu-latest environment: preview-deployments - env: - UNSPLASH_KEY: ${{ secrets.UNSPLASH_KEY }} - UNSPLASH_CLIENT_ID: ${{ secrets.UNSPLASH_CLIENT_ID }} steps: - - name: Check out Git repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Download build artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v4 with: - ref: ${{ github.event.pull_request.head.sha }} + name: pr-preview-build + path: dist/browser + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ github.token }} - - name: Setup Node.js - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 - with: - node-version: 22 - cache: 'npm' + - name: Get PR info + id: pr + env: + GH_TOKEN: ${{ github.token }} + HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }} + HEAD_SHA: ${{ github.event.workflow_run.head_sha }} + HEAD_OWNER: ${{ github.event.workflow_run.head_repository.owner.login }} + REPO_OWNER: ${{ github.event.workflow_run.repository.owner.login }} + REPO: ${{ github.repository }} + run: | + # Handle fork PRs vs same-repo PRs + if [ "$HEAD_OWNER" != "$REPO_OWNER" ]; then + BRANCH="${HEAD_OWNER}:${HEAD_BRANCH}" + else + BRANCH="${HEAD_BRANCH}" + fi - # work around for npm installs from git+https://github.com/johannesjo/J2M.git - - name: Reconfigure git to use HTTP authentication - run: > - git config --global url."https://github.com/".insteadOf - ssh://git@github.com/ + PR_NUMBER=$(gh pr view --repo "$REPO" "$BRANCH" --json number --jq '.number') - - name: Install npm Packages - run: npm i + if ! [[ "$PR_NUMBER" =~ ^[0-9]+$ ]]; then + echo "::error::Could not determine PR number" + exit 1 + fi - - name: Generate environment file - run: npm run env + # Sanitize branch name for safe use in downstream commands + SAFE_BRANCH=$(echo "$HEAD_BRANCH" | sed 's/[^a-zA-Z0-9._\/-]//g') - - name: Build Frontend for Web - run: npm run buildFrontend:prodWeb + echo "number=$PR_NUMBER" >> "$GITHUB_OUTPUT" + echo "head_branch=$SAFE_BRANCH" >> "$GITHUB_OUTPUT" + echo "head_sha=$HEAD_SHA" >> "$GITHUB_OUTPUT" - name: Deploy to Cloudflare Pages id: cloudflare-deploy @@ -52,13 +66,13 @@ jobs: with: apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - command: pages deploy dist/browser --project-name=super-productivity-preview --branch=${{ github.head_ref }} + command: pages deploy dist/browser --project-name=super-productivity-preview --branch=${{ steps.pr.outputs.head_branch }} - name: Find existing comment uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad # v4.0.0 id: find-comment with: - issue-number: ${{ github.event.pull_request.number }} + issue-number: ${{ steps.pr.outputs.number }} comment-author: 'github-actions[bot]' body-includes: '' @@ -66,7 +80,7 @@ jobs: uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0 with: comment-id: ${{ steps.find-comment.outputs.comment-id }} - issue-number: ${{ github.event.pull_request.number }} + issue-number: ${{ steps.pr.outputs.number }} edit-mode: replace body: | @@ -76,8 +90,8 @@ jobs: |--------|-----| | Deployed | ${{ steps.cloudflare-deploy.outputs.deployment-url }} | - **Branch:** `${{ github.head_ref }}` - **Commit:** ${{ github.sha }} + **Branch:** `${{ steps.pr.outputs.head_branch }}` + **Commit:** ${{ steps.pr.outputs.head_sha }} --- Deployed with Cloudflare Pages