Split ci.yml to close a pull_request_target pwn-request hole

pull_request_target checked out fork PR branches with the repo's
write-scoped GITHUB_TOKEN and ran autopep8/metadata scripts/tests
against that fork content, letting a malicious PR rewrite test/*.py
for arbitrary code execution with push access and secrets. It's also
been failing outright for weeks since actions/checkout now blocks
unsafe fork checkouts here without explicit opt-in.

Split into ci-check.yml (plain pull_request, GitHub's read-only
no-secrets token, safe to run fork code) which uploads a diff
artifact, and ci-apply.yml (workflow_run, privileged) which only
applies that diff via `git apply`, never executing fork content.
ci.yml keeps just the push-to-main job as the strict integrity check.

Because GitHub runs the PR's own copy of ci-check.yml for
pull_request events, that artifact is attacker-authored: ci-apply.yml
therefore resolves PR identity from the workflow_run payload plus the
API rather than the artifact, passes every dynamic value through env:
instead of ${{ }} in run: blocks (which the runner substitutes before
the shell parses, so quotes don't contain it), and validates branch,
repo, sha and PR-number shapes before use. The patch itself stays
untrusted input: allowlist-validated and applied only to the fork's
own branch.

test_checks.py adds an env-gated lenient mode so ci-check.yml's
preview run doesn't fail on a brand-new plugin's not-yet-existing
commit sha, while history and push-to-main stay strict.
This commit is contained in:
Loup-Garou911XD 2026-08-09 19:06:41 +05:30
parent d442c44ea8
commit bc5c5aafc8
No known key found for this signature in database
GPG key ID: 296405D97C924E54
4 changed files with 450 additions and 29 deletions

View file

@ -1,20 +1,20 @@
name: CI
# WORD OF CAUTION:
# TO anyone modifying this
# Things will break if you modify this
# without understanding how it works
# A simple flow of this file:
# Apply AutoPEP8 → Apply Plugin Metadata → CRITICAL COMMIT (format + plugin meta)
# ← ← ← ← ← ↵
# ↪ Apply Version Metadata → Commit (version meta) → Tests
# Runs only on pushes to main - i.e. after a PR has been merged, or on a
# direct maintainer push. This is fully trusted, same-repo content, so it's
# safe for it to execute the tree and push directly. This is also the
# AUTHORITATIVE integrity check: test/test_checks.py's test_versions runs
# here unmodified/strict against real, permanent git history (unlike
# ci-check.yml, which can't yet resolve a commit sha for a brand-new plugin
# version and runs leniently instead).
on:
push:
branches:
- main
pull_request_target:
permissions:
contents: write
jobs:
build:
@ -22,9 +22,6 @@ jobs:
steps:
- uses: actions/checkout@v6
with:
token: ${{ secrets.GITHUB_TOKEN }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
ref: ${{ github.head_ref }}
fetch-depth: 0
- name: Set up Python
@ -42,21 +39,10 @@ jobs:
run: |
autopep8 --in-place --recursive --max-line-length=100 .
- name: Apply Plugin Metadata
if: github.event_name == 'pull_request_target'
env:
GH_TOKEN: ${{ github.token }}
run: |
CHANGED_FILES=$(gh api "repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/files" --jq '.[].filename')
python test/auto_apply_plugin_metadata.py "$CHANGED_FILES"
# This is a CRITICAL COMMIT for the next step
# which bases this as the commit to get the sha to store in index.json or plugin.json
- name: Commit Plugin Metadata and AutoPEP8
- name: Commit AutoPEP8 formatting
uses: stefanzweifel/git-auto-commit-action@v7
with:
commit_message: "[ci] apply-plugin-metadata-and-formatting"
branch: ${{ github.head_ref }}
commit_message: "[ci] apply-formatting"
- name: Apply Version Metadata
run: |
@ -66,7 +52,6 @@ jobs:
uses: stefanzweifel/git-auto-commit-action@v7
with:
commit_message: "[ci] apply-version-metadata"
branch: ${{ github.head_ref }}
- name: Execute Tests
run: |