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
59 changes: 59 additions & 0 deletions .github/scripts/Checkout-GhAwPr.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#!/usr/bin/env pwsh
# Checkout-GhAwPr.ps1 — Security-checked PR checkout for gh-aw workflow_dispatch.
#
# Verifies the PR author has write access to the repo, checks out the PR branch,
# then restores .github/ from the base branch (main) to prevent prompt injection
# via modified workflow files in the PR.

$ErrorActionPreference = 'Stop'

$prNumber = $env:PR_NUMBER
if (-not $prNumber) {
Write-Error "PR_NUMBER environment variable is required"
exit 1
}

Write-Host "Checking out PR #$prNumber..."

# Get PR info
$prJson = gh pr view $prNumber --json headRefName,headRepository,headRepositoryOwner,author,baseRefName --repo $env:GITHUB_REPOSITORY 2>&1
if ($LASTEXITCODE -ne 0) {
Write-Error "Failed to get PR #$prNumber info: $prJson"
exit 1
}
$pr = $prJson | ConvertFrom-Json

$branch = $pr.headRefName
$baseBranch = $pr.baseRefName
$author = $pr.author.login

Write-Host "PR #$prNumber by $author, branch: $branch, base: $baseBranch"

# Check author has write access (skip for bots)
if ($author -notmatch '\[bot\]$') {
$permJson = gh api "repos/$($env:GITHUB_REPOSITORY)/collaborators/$author/permission" --jq '.permission' 2>&1
if ($LASTEXITCODE -ne 0) {
Write-Warning "Could not verify author permissions: $permJson"
Comment on lines +35 to +36
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔴 CRITICAL · 3/3 reviewers · Permission check fail-open — API failure silently bypasses access gate

The collaborators API returns HTTP 404 for non-collaborators (the exact users this gate should block). When $LASTEXITCODE -ne 0, the script logs a Write-Warning and continues — the checkout proceeds without any authorization check. This also triggers on rate limits, insufficient token scope, or network errors.

Scenario: A fork PR author with read-only access triggers workflow_dispatch. The API returns 404, the warning is logged and ignored, and the untrusted PR is checked out and reviewed.

Fix: Fail closed — treat API failure as denied:

if ($LASTEXITCODE -ne 0) {
    Write-Error "Could not verify author permissions (non-collaborator or API error): $permJson"
    exit 1
}

} else {
$perm = $permJson.Trim()
if ($perm -notin @('admin', 'maintain', 'write')) {
Write-Error "Author '$author' has '$perm' permission — write access required for workflow_dispatch review"
exit 1
}
Write-Host "Author '$author' has '$perm' access — OK"
}
}

# Fetch and checkout the PR branch
git fetch origin "pull/$prNumber/head:pr-$prNumber" 2>&1 | Write-Host
git checkout "pr-$prNumber" 2>&1 | Write-Host

# Save the PR HEAD SHA
$prSha = git rev-parse HEAD
Write-Host "PR HEAD: $prSha"

# Restore .github/ from the base branch to prevent workflow tampering
Write-Host "Restoring .github/ from $baseBranch..."
git checkout "origin/$baseBranch" -- .github/ 2>&1 | Write-Host
Comment on lines +48 to +57
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔴 CRITICAL · 3/3 reviewers · git exit codes never checked — security contract voided on failure

$ErrorActionPreference = 'Stop' only governs PowerShell cmdlets, not native executables. None of the three git commands check $LASTEXITCODE. If git fetch fails (deleted PR, network error), the script continues — git checkout fails silently, and the .github/ restore (line 57) runs against whatever tree is already in the workspace. The script exits 0 regardless, voiding its core security purpose.

Fix: Check $LASTEXITCODE after each git command:

git fetch origin "pull/$prNumber/head:pr-$prNumber" 2>&1 | Write-Host
if ($LASTEXITCODE -ne 0) { Write-Error "git fetch failed"; exit 1 }

git checkout "pr-$prNumber" 2>&1 | Write-Host
if ($LASTEXITCODE -ne 0) { Write-Error "git checkout failed"; exit 1 }

# ... same for line 57's .github/ restore
git checkout "origin/$baseBranch" -- .github/ 2>&1 | Write-Host
if ($LASTEXITCODE -ne 0) { Write-Error ".github/ restore failed"; exit 1 }

Alternatively, use $PSNativeCommandErrorActionPreference = $true (PowerShell 7.3+).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 MODERATE · 3/3 reviewers · origin/$baseBranch may not exist — .github/ restore can silently fail

The script only fetches pull/$prNumber/head. It never fetches the base branch. In shallow clones or when the PR targets a non-default branch (e.g., release/v2), origin/$baseBranch won't exist locally. Combined with the missing exit-code checks above, this failure is silent and leaves the PR's untrusted .github/ on disk.

Fix: Fetch the base ref before restoring (prefer immutable SHA via baseRefOid):

# Add baseRefOid to the gh pr view --json fields
$baseSha = $pr.baseRefOid
git fetch origin $baseSha 2>&1 | Write-Host
if ($LASTEXITCODE -ne 0) { Write-Error "Failed to fetch base ref"; exit 1 }
git checkout $baseSha -- .github/ 2>&1 | Write-Host
if ($LASTEXITCODE -ne 0) { Write-Error "Failed to restore .github/"; exit 1 }


Write-Host "Checkout complete — PR #$prNumber on branch $branch, .github/ from $baseBranch"
Loading