mirror of
https://github.com/bombsquad-community/plugin-manager.git
synced 2026-08-15 13:03:08 +00:00
fix a loophole
This commit is contained in:
parent
44cffc5cb5
commit
3e0dfccea6
1 changed files with 30 additions and 4 deletions
34
.github/workflows/ci-apply.yml
vendored
34
.github/workflows/ci-apply.yml
vendored
|
|
@ -18,6 +18,14 @@ name: PR Apply
|
||||||
# branch named `a";id;"` becomes live shell. Pass values via `env:` and
|
# branch named `a";id;"` becomes live shell. Pass values via `env:` and
|
||||||
# reference them as "$VAR", which the shell treats as data.
|
# reference them as "$VAR", which the shell treats as data.
|
||||||
#
|
#
|
||||||
|
# 3. NEVER identify the target PR by commit sha alone. A sha is a value, not an
|
||||||
|
# identity: forks share object storage, so anyone can push ANOTHER PR's head
|
||||||
|
# commit onto a branch of their own, open a PR at it, and then close that PR
|
||||||
|
# mid-run so the commit -> PR lookup below resolves to the victim's PR - at
|
||||||
|
# which point this job would push the attacker's artifact to the victim's
|
||||||
|
# branch. The resolved PR must be pinned to workflow_run.head_repository AND
|
||||||
|
# head_branch AND head_sha, so a run can only ever write to its own branch.
|
||||||
|
#
|
||||||
# On trust: a fork PR fully controls ci-check.yml itself (GitHub runs the
|
# On trust: a fork PR fully controls ci-check.yml itself (GitHub runs the
|
||||||
# workflow file from the PR's own merge ref for `pull_request` events - that
|
# workflow file from the PR's own merge ref for `pull_request` events - that
|
||||||
# is why that job gets a read-only, secret-less token). So EVERYTHING in the
|
# is why that job gets a read-only, secret-less token). So EVERYTHING in the
|
||||||
|
|
@ -47,16 +55,21 @@ jobs:
|
||||||
github.event.workflow_run.event == 'pull_request'
|
github.event.workflow_run.event == 'pull_request'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
# Resolves which PR this run belongs to using ONLY trusted inputs:
|
# Resolves which PR this run belongs to using ONLY trusted inputs: the
|
||||||
# workflow_run.head_sha (set by GitHub, not forgeable by the PR author)
|
# workflow_run payload (set by GitHub, not forgeable by the PR author)
|
||||||
# and the REST API. workflow_run.pull_requests is empty for fork PRs,
|
# and the REST API. workflow_run.pull_requests is empty for fork PRs,
|
||||||
# hence the commit -> PR association lookup.
|
# hence the commit -> PR association lookup. That lookup ANSWERS with a
|
||||||
|
# PR but does not PROVE it is this run's PR - a sha can be adopted by any
|
||||||
|
# fork even though it cannot be forged - so the result is pinned to the
|
||||||
|
# payload's head repo/branch/sha below. See HARD RULE 3 in the header.
|
||||||
- name: Resolve and validate PR (trusted sources only)
|
- name: Resolve and validate PR (trusted sources only)
|
||||||
id: pr
|
id: pr
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
REPO: ${{ github.repository }}
|
REPO: ${{ github.repository }}
|
||||||
RUN_HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
|
RUN_HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
|
||||||
|
RUN_HEAD_REPO: ${{ github.event.workflow_run.head_repository.full_name }}
|
||||||
|
RUN_HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
|
|
@ -96,7 +109,20 @@ jobs:
|
||||||
|
|
||||||
[ "$STATE" = "OPEN" ] || skip "PR not open; skipping."
|
[ "$STATE" = "OPEN" ] || skip "PR not open; skipping."
|
||||||
[ "$BASE_REF" = "main" ] || skip "PR not targeting main; skipping."
|
[ "$BASE_REF" = "main" ] || skip "PR not targeting main; skipping."
|
||||||
[ "$HEAD_SHA" = "$RUN_HEAD_SHA" ] || skip "PR head moved since PR Check ran; skipping."
|
|
||||||
|
# HARD RULE 3: pin the resolved PR to the run that produced the
|
||||||
|
# artifact. The sha check alone is not enough - a sha is adoptable by
|
||||||
|
# any fork, so on its own it would let a run push to somebody else's
|
||||||
|
# PR branch. These are compared only against FORK/HEAD_REF, which the
|
||||||
|
# regexes above already validated, so no shape check is needed here:
|
||||||
|
# a null head_repository yields "" and simply fails to match, which is
|
||||||
|
# the fail-closed direction. Do not "simplify" these away.
|
||||||
|
[ "$FORK" = "$RUN_HEAD_REPO" ] \
|
||||||
|
|| skip "Resolved PR head repo != the run's head repo; skipping."
|
||||||
|
[ "$HEAD_REF" = "$RUN_HEAD_BRANCH" ] \
|
||||||
|
|| skip "Resolved PR head branch != the run's head branch; skipping."
|
||||||
|
[ "$HEAD_SHA" = "$RUN_HEAD_SHA" ] \
|
||||||
|
|| skip "PR head moved since PR Check ran; skipping."
|
||||||
|
|
||||||
# maintainerCanModify only has meaning for cross-fork PRs; GitHub
|
# maintainerCanModify only has meaning for cross-fork PRs; GitHub
|
||||||
# reports false for a PR opened from a branch in this same repo,
|
# reports false for a PR opened from a branch in this same repo,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue