GHSL-2025-094: Arbitrary Code Execution via Untrusted Checkout in pull_request_target #
Summary #
| Item | Value |
|---|---|
| Advisory ID | GHSL-2025-094 |
| Severity | Critical |
| Affected Component | ag2ai/faststream |
| CVE | N/A |
| CWE | CWE-94 (Improper Control of Generation of Code) |
| Reference | https://securitylab.github.com/advisories/GHSL-2025-094_ag2ai_faststream/ |
Vulnerability Description #
GHSL-2025-094 is an arbitrary code execution vulnerability that occurs when a GitHub Actions workflow:
- Uses
pull_request_targettrigger (privileged context with write access and secrets) - Checks out code from an untrusted PR head repository
- Executes build/install commands that can run arbitrary code (e.g.,
pip install,npm install)
This allows an attacker to execute arbitrary code with the workflow’s elevated permissions by:
- Creating a malicious fork with a backdoored
setup.pyor similar build file - Submitting a pull request from that fork
- The workflow automatically checks out and builds the attacker’s code
Attack Vector #
sequenceDiagram
participant Attacker
participant Fork
participant MainRepo
participant Workflow
Attacker->>Fork: Create fork with malicious setup.py
Attacker->>MainRepo: Open PR from fork
MainRepo->>Workflow: Trigger pull_request_target workflow
Workflow->>Fork: Checkout attacker's code (head repo)
Workflow->>Workflow: pip install -e . (executes setup.py)
Workflow-->>Attacker: Secrets exfiltrated, repo compromised
Vulnerable Code Pattern #
name: PR Auto Update (Vulnerable)
on:
pull_request_target:
branches:
- main
permissions:
contents: write
pull-requests: write
jobs:
update:
runs-on: ubuntu-latest
steps:
# VULNERABLE: Checkout from untrusted PR head
- name: Checkout PR code
uses: actions/checkout@v4
with:
token: ${{ secrets.AUTOMERGE_TOKEN }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
ref: ${{ github.head_ref }}
# VULNERABLE: Execute untrusted code via pip install
- name: Install dependencies
run: |
pip install -e .
sisakulint Detection #
sisakulint detects this vulnerability pattern with multiple rules:
1. untrusted-checkout #
script/actions/ghsl/ghsl-2025-094.yaml:24:9: untrusted checkout (critical):
'Checkout PR code' checks out potentially untrusted code in a workflow with
privileged triggers [pull_request_target]. Attackers can inject malicious code.
[untrusted-checkout]
This rule identifies when:
- A workflow uses privileged triggers (
pull_request_target,issue_comment,workflow_run) - Code is checked out from untrusted sources (PR head, fork repository)
2. cache-poisoning-poisonable-step #
script/actions/ghsl/ghsl-2025-094.yaml:32:9: cache poisoning risk:
'Install dependencies' runs untrusted code after checking out PR head
(triggers: pull_request_target). Attacker can steal cache tokens
[cache-poisoning-poisonable-step]
3. dangerous-triggers-critical #
script/actions/ghsl/ghsl-2025-094.yaml:7:3: dangerous trigger (critical):
workflow uses privileged trigger(s) [pull_request_target] without any
security mitigations. [dangerous-triggers-critical]
4. artipacked #
script/actions/ghsl/ghsl-2025-094.yaml:24:9: [Medium] actions/checkout without
'persist-credentials: false'. Credentials are stored in .git/config.
[artipacked]
Remediation #
Option 1: Don’t checkout untrusted code in privileged context #
# Use pull_request instead of pull_request_target when possible
on:
pull_request:
branches:
- main
Option 2: Checkout base branch only, then fetch PR changes safely #
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.base.ref }}
# Only copy specific safe files from PR, not executable code
- name: Fetch PR changes
run: |
gh pr diff ${{ github.event.pull_request.number }} > changes.patch
# Review and apply only safe changes
Option 3: Use workflow_call to isolate privileged operations #
# Unprivileged workflow runs tests
name: Test PR
on:
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: pip install -e . && pytest
# Separate privileged workflow only runs after approval
name: Auto Update
on:
workflow_run:
workflows: ["Test PR"]
types:
- completed
jobs:
update:
if: github.event.workflow_run.conclusion == 'success'
# ... privileged operations on base branch only
Option 4: Add strict validation before executing code #
- name: Validate PR source
run: |
# Only allow PRs from trusted collaborators
if [[ "${{ github.event.pull_request.head.repo.full_name }}" != "${{ github.repository }}" ]]; then
echo "External PRs not allowed for auto-update"
exit 1
fi
Test Files #
- Vulnerable pattern:
script/actions/ghsl/ghsl-2025-094.yaml