security: replace pull_request_target with two-workflow pattern (fixes #472)#570
Conversation
|
@CodingAngel1 Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
davidmaronio
left a comment
There was a problem hiding this comment.
the security model is sound, splitting the pull_request_target flow into an unprivileged pr-collector (checks out base.sha, no secrets) + a privileged pr-review gated on trigger phrase + trusted reviewer correctly closes the pwn-request in #472, nice. a few fixes before it works end to end:
- the artifact hand-off is likely broken, pr-review uses actions/download-artifact@v4 with just
name: pr-<num>and no run-id/github-token, v4 only looks in the current run and the collector uploads in a separate pull_request run, so the download won't find it. pass run-id + github-token (or use dawidd6/action-download-artifact). - minor: in the collector's "write metadata json" step the pr title/body are interpolated straight into the jq command via ${{ }}, move them into env: to avoid script-injection (low impact since the collector is unprivileged, but worth it).
- there's leftover hackathon prompt text ("Lernza", "EXTREMELY LENIENT", "ci has been disabled") in the groq system prompt, clean that out.
the required fmt/clippy/test haven't run yet (first-time contributor), i'll approve the run.
- fix(pr-review): replace actions/download-artifact@v4 with
dawidd6/action-download-artifact@v6 (search_artifacts: true) so the
privileged review workflow can find artifacts uploaded by the
separate pr-collector run instead of failing silently
- fix(pr-collector): move pr_title and pr_body out of inline ${{ }}
interpolation in the jq command and into env: vars to prevent
script injection via attacker-controlled PR title/body
- fix(pr-review): remove leftover hackathon/Lernza/CI-disabled text
from the Groq system prompt; replace with ProofOfHeart identity
|
@davidmaronio - All four points addressed in the latest commit (cc4375b): Artifact hand-off — swapped actions/download-artifact@v4 for dawidd6/action-download-artifact@v6 with github_token and search_artifacts: true, so the privileged workflow can locate the artifact uploaded by the separate collector run. Script injection — moved all twelve jq --arg values in the "Write metadata JSON" step out of inline ${{ }} expressions and into env: vars. Agreed it's low impact given the collector is unprivileged, but clean is clean. Stale prompt text — removed the Lernza/hackathon/CI-disabled lines from the Groq system prompt and replaced them with a ProofOfHeart-specific identity. The fraud-detection logic itself is unchanged. CI runs — ready whenever you approve them, thanks. |
davidmaronio
left a comment
There was a problem hiding this comment.
re-reviewed, and you've addressed everything from my last pass, nice work:
- the cross-run artifact fetch now uses dawidd6/action-download-artifact@v6, so pr-review can actually pull the artifact that pr-collector uploaded in its separate run.
- the pr title/body are now passed as env vars (PR_TITLE/PR_BODY) and marshalled with jq -n --arg, so no more ${{ }} interpolation into shell/prompt bodies, script-injection closed.
- the stray "EXTREMELY LENIENT / Lernza" prompt text is gone.
- and the split is sound, pr-collector on pull_request checks out base.sha (untrusted code never runs privileged) and pr-review on pull_request_review holds the secrets.
the reason it showed BLOCKED with no checks was just that GitHub was holding the workflow runs pending maintainer approval (standard for a pr that edits .github/workflows) plus my prior changes-requested, ci.yml itself is untouched so the fmt/clippy/test contexts are fine. i've approved the held runs; once they come back green this is good to merge. approving from my side.
|
update: main was actually broken this whole time, not your pr. PR #574 (per-campaign tokens / milestone withdrawals / emergency pause) got merged on 07-01 while its CI was red and it didn't compile, so every branch that rebased or merged main inherited those exact errors (the duplicate set_emergency_pause_signers, the CreateCampaignParams missing-fields cascade, iter_mut, etc). that wasn't your merge, it was the base. i've reverted #574 in #584 and main is green again. so please rebase onto the current main and those inherited errors will clear: |
…Iris-IV#472) The original auto-review.yml used pull_request_target, which runs in the context of the base repository and therefore has access to repository secrets (OWNER_PAT, GROQ_API_KEY) even when the triggering PR comes from a fork. An attacker could craft a PR that exfiltrates those secrets by manipulating the workflow steps that execute untrusted code from the fork. This commit implements the recommended two-workflow defense: 1. pr-collector.yml — trigger: pull_request (no secrets) Runs entirely without repository secrets. Uses only the ephemeral GITHUB_TOKEN (read-only) to fetch PR metadata and the diff, then uploads everything as a GitHub Actions artifact (retention: 7 days). No untrusted code is ever executed. 2. pr-review.yml — trigger: pull_request_review (secrets gated) Runs in the base-repo context (safe for secrets). Before touching any secret it enforces three independent guards: a. The review must be a submitted review (not a dismissal). b. The review body must contain the trigger phrase /run-auto-review. c. The reviewer must appear in the TRUSTED_REVIEWERS repository variable OR hold at least write/admin collaborator permission. Only after all three pass does the workflow download the artifact and call the Groq API (GROQ_API_KEY) or perform PR actions (OWNER_PAT). The old auto-review.yml.disabled file is deleted; its logic is fully preserved and hardened in the two new files. References: https://securitylab.github.com/research/github-actions-preventing-pwn-requests/
- fix(pr-review): replace actions/download-artifact@v4 with
dawidd6/action-download-artifact@v6 (search_artifacts: true) so the
privileged review workflow can find artifacts uploaded by the
separate pr-collector run instead of failing silently
- fix(pr-collector): move pr_title and pr_body out of inline ${{ }}
interpolation in the jq command and into env: vars to prevent
script injection via attacker-controlled PR title/body
- fix(pr-review): remove leftover hackathon/Lernza/CI-disabled text
from the Groq system prompt; replace with ProofOfHeart identity
cc4375b to
136b8c7
Compare
|
@davidmaronio thank you so much for your patience. The PR has been rebased and awaiting you to run allow it run the CI and merge. Thanks for the heads up and for reverting in #584 |
|
@davidmaronio the adjustments have been made for your review and merge. Thank you. |
…80+ compile soroban-sdk 20.1.0 pins ethnum 1.5.0, which fails on current stable Rust due to a transmute size mismatch with TryFromIntError. Vendor the crate and apply the upstream fix from ethnum v1.5.3 so clippy and tests pass. - Patch crates-io ethnum to vendor/ethnum - Regenerate Cargo.lock Closes CI clippy/test failures on PR Iris-IV#570.
|
@davidmaronio CI Fix Pushed to the PR, the commit is in b3d146c . Pleae review and merge. Thank you |
Summary
Resolves #472 — the
pull_request_targettrigger in.github/workflows/auto-review.ymlexposessecrets.OWNER_PATandsecrets.GROQ_API_KEYto code running in the context of fork PRs, enabling supply-chain attacks.This PR removes the vulnerable workflow and replaces it with the GitHub-recommended two-workflow pattern.
The Vulnerability (CVE class: pwn-request / poisoned pipeline execution)
Root cause
pull_request_targetis a privileged event: it always runs in the context of the base repository, giving the workflow full access to repository secrets, even when the triggering commit comes from a fork. GitHub's documentation explicitly warns:What the old workflow did
A malicious contributor could craft a PR that:
GROQ_API_KEYandOWNER_PATto an attacker-controlled endpoint — e.g., via acurlin a Makefile, a Cargo build script, or a modified shell helper.OWNER_PAT(which has admin-level PR write access) to approve, merge, or close arbitrary PRs, inject malicious commits, or pivot to other repositories the token can access.The workflow was disabled (
.disabledsuffix) but the file remained in the repository; re-enabling it (intentionally or accidentally) would immediately reintroduce the vulnerability.The Fix — Two-Workflow Pattern
The core principle: never mix untrusted code execution with secret access.
Workflow 1 —
pr-collector.yml(unprivileged)pull_requestGITHUB_TOKEN(read-only)gh pr view/diff, serialises to JSON, uploads as a GitHub Actions artifact keyed by PR numberactions/checkoutchecks out the base SHA, not the fork headBy using
ref: ${{ github.event.pull_request.base.sha }}in the checkout step, even the filesystem is free of attacker-controlled code.Workflow 2 —
pr-review.yml(privileged, triple-gated)pull_request_review(submitted)GROQ_API_KEY,OWNER_PAT— but only after all three guards passThe three guards (all must pass before any secret is touched):
/run-auto-review. This prevents the Groq API call from firing on every review comment.TRUSTED_REVIEWERSrepository variable (comma-separated login list), orwriteoradmincollaborator permission on the repository (verified live via the GitHub API).If any guard fails, the workflow logs the reason and exits without touching secrets.
Data flow
Files Changed
.github/workflows/auto-review.yml.disabled.github/workflows/pr-collector.yml.github/workflows/pr-review.ymlNo application code, tests, or CI configuration was changed.
Operational Notes for Maintainers
One-time setup
Set the
TRUSTED_REVIEWERSrepository variable (Settings → Secrets and variables → Actions → Variables):If this variable is absent, the workflow falls back to checking collaborator write/admin permission — which is still safe, just slightly broader.
Ensure
GROQ_API_KEYandOWNER_PATremain configured as repository secrets (they are consumed exactly as before, just in the new privileged workflow).Triggering a review
After a PR lands and
pr-collector.ymlcompletes (the artifact is uploaded), a trusted maintainer submits a review comment containing/run-auto-review. The privileged workflow fires, downloads the artifact, and proceeds exactly as the old workflow did.Artifact retention
Artifacts are retained for 7 days — enough for any review cycle. After that the artifact is deleted automatically by GitHub; re-triggering will fail gracefully with a
download-artifacterror.Security Properties After This Change
OWNER_PATGROQ_API_KEYTesting
The CI workflow (
ci.yml) is unaffected and continues to runcargo fmt,cargo clippy, andcargo teston every PR. The new workflow files are YAML-only; no Rust code was changed.Manual verification checklist (for the reviewer):
pr-collector.ymlhas nosecrets.*referencespr-review.ymlonly references secrets after all three guards evaluate totrueauto-review.yml.disabledis absent from the repositorypr-collector.ymlchecks outbase.sha, nothead.shaCloses #472