Skip to content

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

Open
Loup-Garou911XD wants to merge 3 commits into
bombsquad-community:mainfrom
Loup-Garou911XD:fix/pr-checkout-security
Open

Split ci.yml to close a pull_request_target pwn-request hole#478
Loup-Garou911XD wants to merge 3 commits into
bombsquad-community:mainfrom
Loup-Garou911XD:fix/pr-checkout-security

Conversation

@Loup-Garou911XD

Copy link
Copy Markdown
Member

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.

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.

@Loup-Garou911XD

Copy link
Copy Markdown
Member Author

created by claude
@rikkolovescats @Dliwk pls review

@Loup-Garou911XD
Loup-Garou911XD requested a balanced review from Copilot August 9, 2026 13:51

This comment was marked as off-topic.

This comment was marked as outdated.

@Loup-Garou911XD
Loup-Garou911XD force-pushed the fix/pr-checkout-security branch from 58465c1 to 3572a0d Compare August 9, 2026 15:55
@Loup-Garou911XD
Loup-Garou911XD requested a balanced review from Copilot August 9, 2026 15:57

This comment was marked as off-topic.

This comment was marked as outdated.

@Loup-Garou911XD
Loup-Garou911XD force-pushed the fix/pr-checkout-security branch 3 times, most recently from d70bedd to 704fe9f Compare August 9, 2026 16:21
@Loup-Garou911XD
Loup-Garou911XD requested a lite review from Copilot August 9, 2026 16:53

This comment was marked as outdated.

This comment was marked as outdated.

@Loup-Garou911XD
Loup-Garou911XD force-pushed the fix/pr-checkout-security branch from dd53b01 to 9034d1d Compare August 9, 2026 17:11
@Loup-Garou911XD

Copy link
Copy Markdown
Member Author

Thanks — addressed in 9034d1d.

Staging scope wider than the validation allowlist (both occurrences): fixed. This was a legitimate gap. git add -A -- plugins ... would stage anything dirty under plugins/, including paths the allowlist rejects (e.g. plugins/evil/bad.py, since ALLOW only permits plugins/(minigames|utilities|maps)/*.py).

  • The fixups step now stages exactly the paths the validation step allow-listed, reusing the file it already produced: git add -A --pathspec-from-file="${RUNNER_TEMP}/patch_paths.txt" (plus a guard that refuses to stage if that list is missing/empty). Commit scope can no longer exceed validated scope, by construction.
  • The version-metadata step now stages only index.json and the three category manifests. That script opens every .py read-only and writes only those files, so the narrower pathspec matches exactly what it can modify.

Verified with a scratch repo: with plugins/evil/bad.py and .github/workflows/ci.yml both dirty, only the validated path gets staged; previously the first would have been swept in.

Passing the changed-file list as one CLI argument: not changing, deliberately. Both stated risks are bounded here:

  • ARG_MAX is ~2MB on Linux; a realistic changed-file list is a few KB.
  • git diff --name-only C-quotes paths containing newlines, so they stay on one line and split("\n") does not mis-split. A quoted path simply fails to match plugins/<category>/*.py and is skipped — it fails safe.

More importantly this is in ci-check.yml, which runs with GitHub's read-only, secret-less token, and nothing it emits is trusted: ci-apply.yml independently allow-list-validates the patch. Fixing it "properly" means changing auto_apply_plugin_metadata.py's CLI contract — a script this PR does not otherwise touch and which has other callers — so it is out of scope here. Happy to do it as a follow-up if maintainers want.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.

Suppressed comments (3)

test/test_checks.py:17

  • The comment describes unresolved commits as “absent 40-hex sha raises ValueError”, but this repo stores abbreviated commit SHAs (first 8 chars) in metadata (see test/auto_apply_version_metadata.py). The wording is misleading; make it SHA-length agnostic (rev/hex object name) so future readers don’t assume a 40-hex requirement.
# Raised by Repo.commit() when a sha cannot be resolved: a well-formed but
# absent 40-hex sha raises plain ValueError, while a malformed name raises
# BadName/BadObject (which are NOT ValueError subclasses). Kept narrow so
# lenient mode can't mask unrelated repository errors.

.github/workflows/ci-check.yml:85

  • python test/auto_apply_plugin_metadata.py "$(cat changed_files.txt)" collapses newlines to spaces in bash command substitution, but the script splits on \n (sys.argv[1].split('\n')). This causes the metadata step to miss most changed files. Invoke the script per file (or adjust the script to read the file), so it receives one path per run.
        run: |
          set -euo pipefail
          python test/auto_apply_plugin_metadata.py "$(cat "${RUNNER_TEMP}/changed_files.txt")"

.github/workflows/ci-check.yml:71

  • git diff ... -- '*.py' only matches Python files in the repo root; it won’t include changed plugin files under plugins/**, so autopep8 won’t run on most PR changes. Use a recursive pathspec so all .py files are captured.
          git diff --name-only -z "$BASE_SHA" HEAD -- '*.py' > "${RUNNER_TEMP}/changed_py.z"

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.
@Loup-Garou911XD
Loup-Garou911XD force-pushed the fix/pr-checkout-security branch from 9034d1d to bc5c5aa Compare August 9, 2026 17:26
@Loup-Garou911XD

This comment was marked as outdated.

@Loup-Garou911XD

This comment was marked as resolved.

@Loup-Garou911XD
Loup-Garou911XD requested a lite review from Copilot August 9, 2026 17:30

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@Dliwk

Dliwk commented Aug 9, 2026

Copy link
Copy Markdown
Member

copilot reviewing claude, dead internet theory

auto_apply_plugin_metadata.py asked "what version is already released?" by
reading the category manifest out of the PR's own working tree. Once
ci-apply.yml has pushed its "[ci] apply-plugin-metadata-and-formatting"
commit back to the PR branch, that tree already lists the version being
added - so the check compared the new version against itself and raised
"Version cant be lower or equal than the previous version."

That fires on every re-run of PR Check: the one ci-apply.yml's own push
triggers, and any run caused by a contributor pushing a follow-up commit.

Published state now comes from the base branch instead, read with
`git show $PLUGMAN_BASE_REF:<manifest>` (ci-check.yml supplies the PR base
sha; local runs fall back to origin/main, then to the working tree). The
writer is idempotent to match: a version entry that is already stamped and
still matches the plugin's md5sum is left alone rather than reset to null.

An entry whose md5sum has drifted is still reset, which implements the TODO
this replaces - a contributor can keep iterating on an unpublished version
during review without bumping it every round. Bump enforcement against
published versions is unchanged.

Version comparison and ordering switch to the existing
get_comparable_version_tuple_from_string; versioning_tools.semantic_to_str
ordered 1.0.10 below 1.0.9 and silently truncated 3-digit components.
@Loup-Garou911XD
Loup-Garou911XD force-pushed the fix/pr-checkout-security branch from cb656b9 to 44cffc5 Compare August 10, 2026 08:05
@Loup-Garou911XD
Loup-Garou911XD requested a lite review from Copilot August 10, 2026 08:36

This comment was marked as off-topic.

@Loup-Garou911XD

Copy link
Copy Markdown
Member Author

@bombsquad-community/plugman-maintainers ill merge this tomorrow if no one wants to review it

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants