Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions .github/workflows/pr-cleanup.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
name: Cleanup PR Plugins

on:
pull_request:
types: [closed]

jobs:
cleanup:
runs-on: ubuntu-latest

Check warning on line 9 in .github/workflows/pr-cleanup.yaml

View check run for this annotation

Claude / Claude Code Review

Missing permissions block on cleanup workflow

The cleanup workflow has no explicit `permissions` block, so it inherits the repository's default GITHUB_TOKEN permissions (which may include broad write access). Since this workflow only uses `SQUAREDUP_API_KEY` and never needs the GITHUB_TOKEN, adding `permissions: {}` would enforce least privilege and reduce blast radius.
Comment on lines +7 to +9
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 The cleanup workflow has no explicit permissions block, so it inherits the repository's default GITHUB_TOKEN permissions (which may include broad write access). Since this workflow only uses SQUAREDUP_API_KEY and never needs the GITHUB_TOKEN, adding permissions: {} would enforce least privilege and reduce blast radius.

Extended reasoning...

What the bug is: The pr-cleanup.yaml workflow omits an explicit permissions block entirely. When no permissions block is specified, a GitHub Actions workflow inherits the repository's default GITHUB_TOKEN permissions. Depending on organization and repository settings, those defaults can include write access to contents, issues, pull-requests, and other scopes.

The specific code path: Lines 7–9 define the cleanup job with only runs-on: ubuntu-latest and no permissions: key. The companion workflow pr-run.yaml explicitly declares permissions: pull-requests: write because it needs to post PR comments — demonstrating the team is already aware of the principle of least privilege. The cleanup workflow simply lacks the analogous declaration.

Why existing code doesn't prevent it: There is no permissions key at either the workflow level or the job level in pr-cleanup.yaml. GitHub's default behavior is to grant whatever the repository's default token permissions are, which is an implicit grant rather than an explicit, auditable one.

Impact: The GITHUB_TOKEN is minted and injected into every workflow run automatically. Even though no step currently uses it, a token with broad write permissions exists in the runner environment. If the workflow is ever compromised (e.g., via a malicious dependency in the @squaredup/cli package), the token could be exfiltrated and used to push code, create releases, modify issues, or take other privileged actions against the repository. Setting permissions: {} would make the minted token essentially useless, minimizing blast radius.

How to fix it: Add an explicit empty permissions block to the job (or at the workflow level):

jobs:
  cleanup:
    runs-on: ubuntu-latest
    permissions: {}

Step-by-step proof:

  1. GitHub mints a GITHUB_TOKEN for every workflow run and injects it as secrets.GITHUB_TOKEN and the GITHUB_TOKEN environment variable.
  2. Without a permissions: block, the token's scopes are set by the repository's "Default permissions" setting (Settings → Actions → General). Many repos default to "Read and write permissions" for all scopes.
  3. The pr-cleanup.yaml workflow never calls any GitHub API and has no actions/checkout step — confirming it has zero need for any GITHUB_TOKEN scope.
  4. A supply-chain attack on @squaredup/cli could read GITHUB_TOKEN from the environment and use it to e.g. push a commit or create a release, since the token would carry write permissions.
  5. Adding permissions: {} would cause GitHub to mint a token with no scopes, making it inert even if exfiltrated.

steps:
- name: Install & Configure SquaredUp CLI
env:
SQUAREDUP_API_KEY: ${{ secrets.SQUAREDUP_API_KEY }}
run: |
npm install -g @squaredup/cli
squaredup login --apiKey "$SQUAREDUP_API_KEY"

- name: Delete PR plugins
run: |
pr_number="${{ github.event.pull_request.number }}"
echo "Looking for plugins deployed by PR #${pr_number}..."

plugins=$(squaredup list --json)
matches=$(echo "$plugins" | jq -r --arg pr "-${pr_number}" '.[] | select(.displayName | endswith($pr)) | .id')

if [ -z "$matches" ]; then
echo "No plugins found for PR #${pr_number}."
exit 0
fi

while IFS= read -r id; do
name=$(echo "$plugins" | jq -r --arg id "$id" '.[] | select(.id == $id) | .displayName')
echo "Deleting '${name}' (${id})..."
squaredup delete "${id}"
done <<< "$matches"
Loading