← Actions Artifact Poisoning Check
workflow_run is a trigger that fires when a separate workflow run completes — commonly used so a privileged workflow can react to an unprivileged pull_request build without inheriting the fork's ability to change what that workflow does by editing the workflow file itself. The workflow_run job runs in the context of the base repository, with the base repository's GITHUB_TOKEN and secrets — but the run it reacts to was still triggered by whoever opened the pull request, who might be a stranger.
That is a deliberate, documented design, and it is safe on its own. The risk is what the privileged job does with the artifact the unprivileged run produced:
on:
workflow_run:
workflows: [build-pr]
types: [completed]
jobs:
publish-results:
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
- run: |
chmod +x ./test-results/publish.sh
./test-results/publish.sh
The artifact was uploaded by the fork-triggered build-pr workflow, so its contents — not just the event payload GitHub itself controls — are whatever the fork put there. Downloading it is not the risk; running it is. GitHub's own Actions Security Lab, and independent research from Wiz, Legit Security and Orca Security, document this pattern as "artifact poisoning": a fork can put a script in the artifact it uploads and rely on the privileged handler to execute it unchecked.
A download-artifact step with no name: input pulls in every artifact the triggering run uploaded, not one specific, expected one. Even if nothing in the same job executes anything directly, that widens what untrusted content is sitting on disk for a later step, or an action called by uses:, to pick up.
eval anything read from a downloaded artifact. Treat it as data: read specific, expected fields and validate their shape before use.pull_request, no secrets) and have the privileged job read only reviewed, structured results — not executable content.name: on every download-artifact step so it fetches exactly the artifact you expect, not everything the triggering run happened to upload.Paste your workflow YAML into Actions Artifact Poisoning Check to find every workflow_run job that downloads and later executes an artifact, and every unscoped download, across every workflow file at once, or run the CLI in your own repository:
python3 check.py --fail-on-risk
That exits with a nonzero status if any workflow has either pattern — wire it into your own CI to block a newly-added one before it merges.