Skip to content

security: replace pull_request_target with two-workflow pattern (fixes #472)#570

Merged
davidmaronio merged 3 commits into
Iris-IV:mainfrom
CodingAngel1:security/fix-pwn-request-472
Jul 12, 2026
Merged

security: replace pull_request_target with two-workflow pattern (fixes #472)#570
davidmaronio merged 3 commits into
Iris-IV:mainfrom
CodingAngel1:security/fix-pwn-request-472

Conversation

@CodingAngel1

Copy link
Copy Markdown
Contributor

Summary

Resolves #472 — the pull_request_target trigger in .github/workflows/auto-review.yml exposes secrets.OWNER_PAT and secrets.GROQ_API_KEY to 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_target is 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:

"If the pull_request_target event is used with the pull_request trigger, the workflow will have write access to the target repository's resources, including secrets."

What the old workflow did

on:
  pull_request_target:            # <- privileged event
    types: [opened, synchronize, ...]

permissions:
  contents: write
  pull-requests: write

env:
  GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}   # <- secret injected
  GH_TOKEN:     ${{ secrets.OWNER_PAT }}       # <- PAT injected

A malicious contributor could craft a PR that:

  1. Modifies the workflow itself (before it was disabled) or any script invoked by it.
  2. Causes the runner to exfiltrate GROQ_API_KEY and OWNER_PAT to an attacker-controlled endpoint — e.g., via a curl in a Makefile, a Cargo build script, or a modified shell helper.
  3. Uses the leaked 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 (.disabled suffix) 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)

Property Value
Trigger pull_request
Runs in context of fork commit (untrusted)
Secrets available None — only ephemeral GITHUB_TOKEN (read-only)
What it does Fetches PR metadata + diff via gh pr view/diff, serialises to JSON, uploads as a GitHub Actions artifact keyed by PR number
Untrusted code executed? No — actions/checkout checks out the base SHA, not the fork head

By 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)

Property Value
Trigger pull_request_review (submitted)
Runs in context of Base repository (safe for secrets)
Secrets available GROQ_API_KEY, OWNER_PAT — but only after all three guards pass
What it does Downloads the artifact, calls Groq, posts review, optionally merges/closes

The three guards (all must pass before any secret is touched):

  1. Review state — the triggering review must be a real submission (approved / changes_requested / commented). Dismissed reviews are skipped.
  2. Trigger phrase — the review body must contain the literal string /run-auto-review. This prevents the Groq API call from firing on every review comment.
  3. Trusted reviewer — the reviewer must either:
    • Appear in the TRUSTED_REVIEWERS repository variable (comma-separated login list), or
    • Hold write or admin collaborator 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

Fork PR opened / updated
        |
        v
+-----------------------------------------------------+
|  pr-collector.yml  (pull_request — NO secrets)      |
|  * Checks out base SHA (not fork code)              |
|  * Fetches metadata + diff via GITHUB_TOKEN         |
|  * Uploads artifact: pr-{number}/                   |
|    +-- pr-meta.json                                 |
|    +-- pr-diff-truncated.txt                        |
|    +-- issues-context.txt                           |
+-----------------------------------------------------+
                            |  artifact
                            v
  Trusted maintainer leaves review with `/run-auto-review`
                            |
                            v
+-----------------------------------------------------+
|  pr-review.yml  (pull_request_review — HAS secrets) |
|  Guard 1: review state is actionable?               |
|  Guard 2: body contains /run-auto-review?           |
|  Guard 3: reviewer is trusted?                      |
|       +- NO to any -> exit, log reason              |
|       +- YES to all -> download artifact            |
|            -> call Groq (GROQ_API_KEY)              |
|            -> post review / merge / close (OWNER_PAT)|
+-----------------------------------------------------+

Files Changed

File Action Reason
.github/workflows/auto-review.yml.disabled Deleted Removes the vulnerable workflow permanently
.github/workflows/pr-collector.yml Added Unprivileged collector (Workflow 1)
.github/workflows/pr-review.yml Added Privileged reviewer, triple-gated (Workflow 2)

No application code, tests, or CI configuration was changed.


Operational Notes for Maintainers

One-time setup

  1. Set the TRUSTED_REVIEWERS repository variable (Settings → Secrets and variables → Actions → Variables):

    Name:  TRUSTED_REVIEWERS
    Value: your-github-login,other-maintainer
    

    If this variable is absent, the workflow falls back to checking collaborator write/admin permission — which is still safe, just slightly broader.

  2. Ensure GROQ_API_KEY and OWNER_PAT remain 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.yml completes (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-artifact error.


Security Properties After This Change

Threat Before After
Fork PR exfiltrates OWNER_PAT Possible Impossible — PAT only used in privileged workflow, never touches fork code
Fork PR exfiltrates GROQ_API_KEY Possible Impossible — same isolation
Attacker merges arbitrary PRs via leaked PAT Possible Impossible
Reviewer impersonation triggers Groq call N/A Prevented — collaborator permission check
Automated trigger flooding Groq API N/A Prevented — explicit trigger phrase required

Testing

The CI workflow (ci.yml) is unaffected and continues to run cargo fmt, cargo clippy, and cargo test on every PR. The new workflow files are YAML-only; no Rust code was changed.

Manual verification checklist (for the reviewer):

  • pr-collector.yml has no secrets.* references
  • pr-review.yml only references secrets after all three guards evaluate to true
  • auto-review.yml.disabled is absent from the repository
  • pr-collector.yml checks out base.sha, not head.sha

Closes #472

@drips-wave

drips-wave Bot commented Jun 30, 2026

Copy link
Copy Markdown

@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! 🚀

Learn more about application limits

@davidmaronio davidmaronio left a comment

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.

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:

  1. 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).
  2. 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).
  3. 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.

CodingAngel1 added a commit to CodingAngel1/ProofOfHeart-stellar that referenced this pull request Jul 4, 2026
- 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
@CodingAngel1

Copy link
Copy Markdown
Contributor Author

@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.

@CodingAngel1 CodingAngel1 requested a review from davidmaronio July 4, 2026 21:39

@davidmaronio davidmaronio left a comment

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.

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.

@davidmaronio

Copy link
Copy Markdown
Contributor

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: git fetch origin && git rebase origin/main && git push --force-with-lease. (#574's feature will be re-submitted properly.) sorry for the earlier note pinning it on your rebase.

…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
@CodingAngel1 CodingAngel1 force-pushed the security/fix-pwn-request-472 branch from cc4375b to 136b8c7 Compare July 6, 2026 05:39
@CodingAngel1

CodingAngel1 commented Jul 6, 2026

Copy link
Copy Markdown
Contributor Author

@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

@CodingAngel1 CodingAngel1 requested a review from davidmaronio July 7, 2026 12:21
@CodingAngel1

Copy link
Copy Markdown
Contributor Author

@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.
@CodingAngel1

Copy link
Copy Markdown
Contributor Author

@davidmaronio CI Fix Pushed to the PR, the commit is in b3d146c . Pleae review and merge. Thank you

@davidmaronio davidmaronio merged commit db83100 into Iris-IV:main Jul 12, 2026
4 checks passed
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.

[Security] Groq AI auto-reviewer has write access to production secrets via pull_request_target — supply chain risk

2 participants