Coordinated Disclosure Timeline
- 2026-08-27: The report was sent to security at spiffe.io.
- 2026-09-04: The pull request with a fix was merged.
Summary
The assign-reviewer-review-apply workflow downloads and extracts an artifact produced by a fork-controlled workflow directly into a workspace containing trusted executable code. An external pull request author can place a replacement .github/workflows/scripts/assign_reviewer_lib.js in that artifact. The privileged workflow_run job subsequently imports the overwritten module before validating the artifact data, resulting in arbitrary JavaScript execution with a GitHub token that has issues: write and pull-requests: write permissions.
Project
spiffe/spire repository
Tested Version
Observed on the default branch at commit ef8527ec91013c6c1b4741e0f6ac0786576e620e.
Details
Poisoned workflow artifact overwrites a trusted helper and enables code execution in a privileged job (GHSL-2026-204)
The assign-reviewer-review-capture workflow runs whenever a pull request review is submitted, without filtering by review state or submitter. GitHub prevents a pull request author from approving their own pull request, but the author can submit a comment-only review. That action produces a submitted review with state commented, which the apply workflow accepts for the pull request author. Consequently, an external contributor can trigger the capture workflow on their own pull request without another reviewer; a maintainer may only need to approve the fork workflow run under the repository’s GitHub Actions policy. For a pull request from a fork, the external contributor can also modify the pull request’s copy of the capture workflow. Although the producer receives only a read-scoped token, it can upload attacker-chosen files under the expected artifact name assign-reviewer-review instead of the intended review.json:
on:
pull_request_review:
types: [submitted]
permissions:
contents: read
jobs:
capture:
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const fs = require('fs');
const review = context.payload.review;
const data = {
pr_number: context.payload.pull_request.number,
review_state: (review.state || '').toLowerCase(),
submitter: review.user.login,
};
fs.writeFileSync('review.json', JSON.stringify(data));
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: assign-reviewer-review
path: review.json
Successful completion of the named capture workflow triggers the default-branch assign-reviewer-review-apply workflow. This privileged consumer has write access to upstream issues and pull requests:
on:
workflow_run:
workflows: [assign-reviewer-review-capture]
types: [completed]
jobs:
apply:
if: github.event.workflow_run.conclusion == 'success'
permissions:
contents: read
issues: write
pull-requests: write
actions: read
The apply job first sparse-checks out the trusted workflow scripts. It then downloads and extracts the artifact into the same workspace without specifying an isolated path:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
sparse-checkout: .github/workflows/scripts
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: assign-reviewer-review
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ github.token }}
Because the fork-controlled producer determines the artifact entries, it can include .github/workflows/scripts/assign_reviewer_lib.js. Extraction into the workspace replaces the trusted helper that was just checked out.
The following actions/github-script step imports and invokes that helper before reading or validating review.json:
const fs = require('fs');
const courtLib = require('./.github/workflows/scripts/assign_reviewer_lib.js');
const court = await courtLib({ github, context, core });
const { owner, repo } = context.repo;
// Artifact field validation starts only after the module has executed.
let raw;
try {
raw = JSON.parse(fs.readFileSync('review.json', 'utf8'));
} catch (e) {
core.warning(`Could not read captured review data: ${e.message}`);
return;
}
The later checks validate pr_number, review_state, submitter, the pull request head SHA, and the existence of the claimed review. Those checks constrain attacks that only forge review.json, but they cannot protect a helper module that has already executed.
A representative attack sequence is:
- An external contributor modifies the capture workflow in a fork pull request so that a successful run uploads an artifact named
assign-reviewer-reviewcontaining a malicious.github/workflows/scripts/assign_reviewer_lib.js. - The contributor submits a comment-only review on their own pull request. This creates a
pull_request_review: submittedevent with statecommentedand triggers the modified capture workflow, subject only to any GitHub Actions approval required for the fork run; no approving review or separate reviewer is required. - Successful completion automatically triggers the default-branch
workflow_runapply job with issue and pull-request write permissions. - The apply job checks out the trusted helper, then extracts the attacker-controlled artifact over the same workspace.
actions/github-scriptimports the replaced helper and supplies its authenticatedgithubclient before any artifact-data validation occurs.
Impact
An external fork pull request author can execute arbitrary JavaScript in the privileged apply job and use its authenticated GitHub client to perform operations allowed by issues: write and pull-requests: write. This compromises the integrity of upstream issues and pull requests, including labels, assignees, and other writable issue or pull request state.
CWEs
Resources
assign-reviewer-review-captureproducer workflowassign-reviewer-review-applyprivileged consumer workflow- GitHub Security Lab: Preventing pwn requests
Credit
This issue was discovered and reported by GHSL team member @JarLob (Jaroslav Lobačevski).
Contact
You can contact the GHSL team at securitylab@github.com. Please include a reference to GHSL-2026-204 in any communication regarding this issue.