diff --git a/.github/workflows/goose-issue-solver.yml b/.github/workflows/goose-issue-solver.yml index e6619564e8..8c36fdeeaf 100644 --- a/.github/workflows/goose-issue-solver.yml +++ b/.github/workflows/goose-issue-solver.yml @@ -181,9 +181,11 @@ jobs: run: | echo "number=$(jq -r '.number' /tmp/issue.json)" >> $GITHUB_OUTPUT - echo "title<> $GITHUB_OUTPUT + # SECURITY: Use random delimiter to prevent injection if title contains our delimiter + DELIMITER="EOF_$(openssl rand -hex 8)" + echo "title<<$DELIMITER" >> $GITHUB_OUTPUT jq -r '.title' /tmp/issue.json >> $GITHUB_OUTPUT - echo "TITLE_EOF" >> $GITHUB_OUTPUT + echo "$DELIMITER" >> $GITHUB_OUTPUT - name: Run goose id: goose @@ -202,9 +204,11 @@ jobs: if [ -n "$(git status --porcelain)" ] && [ -f /tmp/issue_summary.txt ]; then echo "has_changes=true" >> $GITHUB_OUTPUT - echo "summary<> $GITHUB_OUTPUT + # SECURITY: Use random delimiter to prevent injection if summary contains our delimiter + SUMMARY_DELIMITER="EOF_$(openssl rand -hex 8)" + echo "summary<<$SUMMARY_DELIMITER" >> $GITHUB_OUTPUT cat /tmp/issue_summary.txt >> $GITHUB_OUTPUT - echo "SUMMARY_EOF" >> $GITHUB_OUTPUT + echo "$SUMMARY_DELIMITER" >> $GITHUB_OUTPUT else echo "has_changes=false" >> $GITHUB_OUTPUT fi diff --git a/.github/workflows/goose-pr-reviewer.yml b/.github/workflows/goose-pr-reviewer.yml index d1dfd2a605..fc5b7aba3e 100644 --- a/.github/workflows/goose-pr-reviewer.yml +++ b/.github/workflows/goose-pr-reviewer.yml @@ -274,9 +274,11 @@ jobs: INSTRUCTIONS="No specific instructions - perform a general code review." fi - echo "instructions<> $GITHUB_OUTPUT + # SECURITY: Use random delimiter to prevent injection if comment contains our delimiter + DELIMITER="EOF_$(openssl rand -hex 8)" + echo "instructions<<$DELIMITER" >> $GITHUB_OUTPUT echo "$INSTRUCTIONS" >> $GITHUB_OUTPUT - echo "INSTRUCTIONS_EOF" >> $GITHUB_OUTPUT + echo "$DELIMITER" >> $GITHUB_OUTPUT - name: Run goose review id: goose @@ -285,14 +287,16 @@ jobs: PR_TITLE: ${{ github.event.issue.title }} PR_BODY: ${{ github.event.issue.body }} REVIEW_INSTRUCTIONS: ${{ steps.instructions.outputs.instructions }} + # SECURITY: Pass issue JSON via environment variable to avoid heredoc injection + # (GHSA-mm8p-57gq-3xj6) - user-controlled content could terminate heredoc early + ISSUE_JSON: ${{ toJson(github.event.issue) }} run: | mkdir -p $HOME/.local/share/goose/sessions mkdir -p $HOME/.config/goose git config --global --add safe.directory "$GITHUB_WORKSPACE" - cat > /tmp/pr.json << 'PRJSON' - ${{ toJson(github.event.issue) }} - PRJSON + # SECURITY: Use printf with env var instead of heredoc to prevent injection + printf '%s' "$ISSUE_JSON" > /tmp/pr.json echo "$GOOSE_RECIPE" | envsubst '$PR_NUMBER $PR_TITLE $PR_BODY $REVIEW_INSTRUCTIONS' > /tmp/recipe.yaml diff --git a/.github/workflows/pr-comment-build-cli.yml b/.github/workflows/pr-comment-build-cli.yml index ac7e7c6749..510fe0f723 100644 --- a/.github/workflows/pr-comment-build-cli.yml +++ b/.github/workflows/pr-comment-build-cli.yml @@ -1,4 +1,8 @@ # This workflow is triggered by a comment on PR with the text ".build-cli" +# +# SECURITY: This workflow checks out and builds code from PRs. To prevent +# malicious code execution (GHSA-4h72-4h3w-4587, GHSA-mqm8-hhf6-wvjq), +# we verify the commenter has write access before proceeding. on: issue_comment: types: [created] @@ -28,11 +32,56 @@ jobs: name: Trigger on ".build-cli" PR comment runs-on: ubuntu-latest outputs: - continue: 'true' + continue: ${{ steps.security_check.outputs.authorized }} pr_number: ${{ steps.command.outputs.issue_number || github.event.inputs.pr_number }} head_sha: ${{ steps.set_head_sha.outputs.head_sha || github.sha }} steps: + # SECURITY: Verify commenter has write access BEFORE any checkout + # This prevents attackers from triggering builds on their own malicious PRs + - name: Verify commenter permissions + id: security_check + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + script: | + // workflow_dispatch requires repo write access, so it's inherently safe + if (context.eventName === 'workflow_dispatch') { + core.setOutput('authorized', 'true'); + console.log('✅ workflow_dispatch - authorized'); + return; + } + + const commenter = context.payload.comment.user.login; + console.log(`Checking permissions for: ${commenter}`); + + try { + const { data: permission } = await github.rest.repos.getCollaboratorPermissionLevel({ + owner: context.repo.owner, + repo: context.repo.repo, + username: commenter + }); + + const allowed = ['admin', 'maintain', 'write'].includes(permission.permission); + console.log(`Permission level: ${permission.permission}, Authorized: ${allowed}`); + + if (!allowed) { + // Post a comment explaining the rejection + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.issue.number, + body: `⚠️ @${commenter} Only repository collaborators with write access can trigger builds.` + }); + core.setOutput('authorized', 'false'); + } else { + core.setOutput('authorized', 'true'); + } + } catch (error) { + console.log(`Permission check failed: ${error.message}`); + core.setOutput('authorized', 'false'); + } + - name: Run command action + if: steps.security_check.outputs.authorized == 'true' uses: github/command@v2.0.3 id: command with: @@ -42,10 +91,12 @@ jobs: allowed_contexts: pull_request - name: Checkout code + if: steps.security_check.outputs.authorized == 'true' uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - name: Get PR head SHA with gh id: set_head_sha + if: steps.security_check.outputs.authorized == 'true' run: | echo "Get PR head SHA with gh" HEAD_SHA=$(gh pr view "$ISSUE_NUMBER" --json headRefOid -q .headRefOid) @@ -92,4 +143,4 @@ jobs: - [📦 Windows (x86_64)](https://nightly.link/${{ github.repository }}/actions/runs/${{ github.run_id }}/goose-x86_64-pc-windows-gnu.zip) These links are provided by nightly.link and will work even if you're not logged into GitHub. - \ No newline at end of file + diff --git a/.github/workflows/pr-comment-bundle.yml b/.github/workflows/pr-comment-bundle.yml index 68beede33a..5d315fbc87 100644 --- a/.github/workflows/pr-comment-bundle.yml +++ b/.github/workflows/pr-comment-bundle.yml @@ -1,7 +1,12 @@ # This workflow is triggered by a comment on PR with the text ".bundle" # It bundles the ARM64 Desktop App, then creates a PR comment with a link to download the app. - +# +# SECURITY: This workflow checks out and builds code from PRs. To prevent +# malicious code execution (GHSA-4h72-4h3w-4587), we verify the commenter +# has write access before proceeding. on: + issue_comment: + types: [created] workflow_dispatch: inputs: pr_number: @@ -28,11 +33,56 @@ jobs: name: Trigger on ".bundle" PR comment runs-on: ubuntu-latest outputs: - continue: 'true' + continue: ${{ steps.security_check.outputs.authorized }} pr_number: ${{ steps.command.outputs.issue_number || github.event.inputs.pr_number }} pr_sha: ${{ steps.get_pr_info.outputs.sha }} steps: + # SECURITY: Verify commenter has write access BEFORE any checkout + # This prevents attackers from triggering builds on their own malicious PRs + - name: Verify commenter permissions + id: security_check + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + script: | + // workflow_dispatch requires repo write access, so it's inherently safe + if (context.eventName === 'workflow_dispatch') { + core.setOutput('authorized', 'true'); + console.log('✅ workflow_dispatch - authorized'); + return; + } + + const commenter = context.payload.comment.user.login; + console.log(`Checking permissions for: ${commenter}`); + + try { + const { data: permission } = await github.rest.repos.getCollaboratorPermissionLevel({ + owner: context.repo.owner, + repo: context.repo.repo, + username: commenter + }); + + const allowed = ['admin', 'maintain', 'write'].includes(permission.permission); + console.log(`Permission level: ${permission.permission}, Authorized: ${allowed}`); + + if (!allowed) { + // Post a comment explaining the rejection + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.issue.number, + body: `⚠️ @${commenter} Only repository collaborators with write access can trigger builds.` + }); + core.setOutput('authorized', 'false'); + } else { + core.setOutput('authorized', 'true'); + } + } catch (error) { + console.log(`Permission check failed: ${error.message}`); + core.setOutput('authorized', 'false'); + } + - name: Debug workflow trigger + if: steps.security_check.outputs.authorized == 'true' env: WORKFLOW_NAME: ${{ github.workflow }} WORKFLOW_REF: ${{ github.ref }} @@ -50,6 +100,7 @@ jobs: echo "Repository: ${REPOSITORY}" - name: Run command action + if: steps.security_check.outputs.authorized == 'true' uses: github/command@3442f3fa1efe01bdb024b157083c337902d17372 # v2.0.3 id: command with: @@ -61,7 +112,7 @@ jobs: # Get the PR's SHA - name: Get PR info id: get_pr_info - if: ${{ steps.command.outputs.continue == 'true' || github.event_name == 'workflow_dispatch' }} + if: steps.security_check.outputs.authorized == 'true' uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | @@ -152,4 +203,4 @@ jobs: * optionally run `codesign --force --deep --sign - --entitlements ui/desktop/entitlements.plist '/path/to/Goose.app'` * start the app - The signing step is only needed if you do something that uses mac entitlements like speech to text \ No newline at end of file + The signing step is only needed if you do something that uses mac entitlements like speech to text diff --git a/.github/workflows/recipe-security-scanner.yml b/.github/workflows/recipe-security-scanner.yml index 7eefc76487..e6f2207c57 100644 --- a/.github/workflows/recipe-security-scanner.yml +++ b/.github/workflows/recipe-security-scanner.yml @@ -25,14 +25,29 @@ jobs: with: egress-policy: audit - - name: Checkout PR + # SECURITY FIX (GHSA-7qhh-cph9-6ppm): Checkout base branch for trusted Dockerfile + # The PR could contain a malicious Dockerfile that exfiltrates secrets. + # We checkout the base branch (trusted) for building the scanner image, + # and only fetch recipe files from the PR for scanning. + - name: Checkout base branch (trusted code) + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + ref: ${{ github.event.pull_request.base.sha }} + fetch-depth: 0 + path: trusted + + - name: Fetch PR recipe files only uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: ref: ${{ github.event.pull_request.head.sha }} fetch-depth: 0 + path: pr-content + sparse-checkout: | + documentation/src/pages/recipes/data/recipes/ - name: Check if recipe files changed in this push id: recipe_changes + working-directory: pr-content run: | set -e echo "🔍 Checking if recipe files were modified in this push..." @@ -68,6 +83,7 @@ jobs: - name: Find recipe files in PR (new or modified) id: find_recipes if: steps.recipe_changes.outputs.recipe_files_changed == 'true' + working-directory: pr-content run: | set -e echo "Looking for recipe files in PR (new or modified)..." @@ -97,8 +113,8 @@ jobs: echo "has_recipes=true" >> "$GITHUB_OUTPUT" echo "recipe_count=$RECIPE_COUNT" >> "$GITHUB_OUTPUT" - # Save recipe file paths for later steps - echo "$RECIPE_FILES" > "$RUNNER_TEMP/recipe_files.txt" + # Save recipe file paths for later steps (with pr-content prefix for mounting) + echo "$RECIPE_FILES" | sed 's|^|pr-content/|' > "$RUNNER_TEMP/recipe_files.txt" fi - name: Set up Docker Buildx @@ -117,14 +133,15 @@ jobs: DOCKER_BUILDKIT: 1 IMAGE_TAG: ${{ github.sha }} run: | + # SECURITY: Build from trusted/ directory (base branch) to prevent malicious Dockerfile docker buildx build \ --pull \ --no-cache \ --load \ --platform linux/amd64 \ -t "recipe-scanner:${IMAGE_TAG}" \ - -f recipe-scanner/Dockerfile \ - recipe-scanner/ + -f trusted/recipe-scanner/Dockerfile \ + trusted/recipe-scanner/ - name: Scan all recipe files if: steps.find_recipes.outputs.has_recipes == 'true' && steps.recipe_changes.outputs.recipe_files_changed == 'true'