GHSL-2025-082: Cache Poisoning via Local Action Execution #
Summary #
| Item | Value |
|---|---|
| Advisory ID | GHSL-2025-082 |
| Severity | Critical |
| Affected Component | ag-grid/ag-grid |
| CVE | N/A |
| CWE | CWE-829 (Inclusion of Functionality from Untrusted Control Sphere) |
| Reference | https://securitylab.github.com/advisories/GHSL-2025-082_ag-grid_ag-grid/ |
Vulnerability Description #
GHSL-2025-082 is a code execution vulnerability in GitHub Actions workflows that occurs when:
- A workflow is triggered by a privileged event (
issue_comment,pull_request_target,workflow_run) - The workflow checks out untrusted PR code using
ref: ${{ github.event.pull_request.head.sha }} - After checkout, the workflow executes a local action (e.g.,
./.github/actions/setup-nx)
This pattern allows attackers to execute arbitrary code with access to repository secrets by modifying the local action in their fork.
Attack Vector #
sequenceDiagram
participant Attacker
participant Fork
participant PR
participant Workflow
participant Secrets
Attacker->>Fork: Create fork of repository
Attacker->>Fork: Modify .github/actions/setup-nx with malicious code
Attacker->>PR: Create PR from fork to upstream
Attacker->>PR: Post comment to trigger issue_comment
PR->>Workflow: Trigger issue_comment event
Workflow->>Workflow: Checkout PR head (attacker's code)
Workflow->>Workflow: Execute ./.github/actions/setup-nx (malicious)
Workflow->>Secrets: Access SLACK_BOT_OAUTH_TOKEN, JIRA_API_AUTH
Workflow->>Attacker: Exfiltrate secrets via malicious action
Vulnerable Code Pattern #
name: Performance Workflow
on:
issue_comment:
types: [created]
permissions:
pull-requests: write
issues: write
jobs:
performance:
runs-on: ubuntu-latest
steps:
# VULNERABLE: Checkout PR head code (untrusted)
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
# VULNERABLE: Execute local action from checked out code
# This action is now controlled by the attacker!
- name: Setup
uses: ./.github/actions/setup-nx
# Secrets are now compromised
- name: Post to Slack
uses: slackapi/slack-github-action@v1
env:
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_OAUTH_TOKEN }}
Why This Is Dangerous #
- Untrusted Checkout:
ref: ${{ github.event.pull_request.head.sha }}checks out the PR author’s code - Local Action Execution:
./.github/actions/setup-nxis read from the checked-out code - Attacker Control: The attacker can replace the local action with arbitrary malicious code
- Secret Access: The malicious action runs with access to all workflow secrets
sisakulint Detection #
sisakulint detects this vulnerability with the cache-poisoning-poisonable-step rule:
Detection Output #
script/actions/ghsl/ghsl-2025-082.yaml:36:9: cache poisoning risk via local action:
'./.github/actions/setup-nx' runs untrusted code after checking out PR head
(triggers: issue_comment). Attacker can steal cache tokens [cache-poisoning-poisonable-step]
script/actions/ghsl/ghsl-2025-082.yaml:54:9: cache poisoning risk via local script
execution: 'run: ./scripts/setup.sh' runs untrusted code after checking out PR head
(triggers: issue_comment). Attacker can steal cache tokens [cache-poisoning-poisonable-step]
script/actions/ghsl/ghsl-2025-082.yaml:65:9: cache poisoning risk via build command:
'run: npm install' runs untrusted code after checking out PR head
(triggers: issue_comment). Attacker can steal cache tokens [cache-poisoning-poisonable-step]
Detection Conditions #
The rule triggers when ALL of the following conditions are met:
| Condition | Description |
|---|---|
| Unsafe Trigger | issue_comment, pull_request_target, or workflow_run |
| Unsafe Checkout | ref contains github.event.pull_request.head.sha/ref |
| Poisonable Step | Local action (./), local script, or build command after checkout |
Poisonable Step Types #
| Type | Pattern | Example |
|---|---|---|
| Local Action | uses: ./... | ./.github/actions/setup-nx |
| Local Script | ./script.sh | ./scripts/setup.sh |
| Build Command | npm install, make, etc. | npm install, pip install |
| GitHub Script Import | require('./local') | require('./lib/helper') |
Remediation #
Option 1: Remove Unsafe Ref (Recommended) #
- uses: actions/checkout@v4
# Don't specify ref - defaults to safe base branch
- name: Setup
uses: ./.github/actions/setup-nx
Option 2: Use Safe Ref #
- uses: actions/checkout@v4
with:
ref: ${{ github.sha }} # Base branch SHA, not PR head
- name: Setup
uses: ./.github/actions/setup-nx
Option 3: Split Privileged and Unprivileged Work #
# Workflow 1: Run performance tests (unprivileged)
name: Performance Tests
on: pull_request
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm run perf-test
- uses: actions/upload-artifact@v4
with:
name: results
path: results.json
---
# Workflow 2: Post results (privileged)
name: Post Results
on:
workflow_run:
workflows: ["Performance Tests"]
types: [completed]
jobs:
post:
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
# Process results safely (no untrusted code execution)
Option 4: Require Approval for External Contributors #
Configure repository settings to require approval before running workflows from first-time contributors.
Option 5: Restrict to Collaborators #
jobs:
performance:
if: |
github.event.comment.author_association == 'OWNER' ||
github.event.comment.author_association == 'MEMBER' ||
github.event.comment.author_association == 'COLLABORATOR'
Additional Security Considerations #
Never Execute Untrusted Code with Secrets #
After checking out untrusted code, any of the following can execute attacker-controlled code:
- Local actions (
./.github/actions/*) - Local scripts (
./scripts/*.sh) - Build tools (
npm install,pip install,make, etc.) - Interpreters running local files (
node ./script.js,python ./script.py)
Minimize Secret Exposure #
Only pass secrets to steps that absolutely need them:
# BAD: Secret available to all steps
env:
SLACK_TOKEN: ${{ secrets.SLACK_TOKEN }}
# GOOD: Secret only available to specific step
- name: Post to Slack
env:
SLACK_TOKEN: ${{ secrets.SLACK_TOKEN }}
run: ...
Use workflow_run Pattern for Safe PR Workflows #
The workflow_run pattern separates untrusted code execution from privileged operations:
- First workflow: Triggered by
pull_request, runs untrusted code - Second workflow: Triggered by
workflow_run, has secrets but doesn’t run untrusted code
Auto-Fix Support #
sisakulint provides auto-fix for this vulnerability:
# Preview the fix
sisakulint -fix dry-run script/actions/ghsl/ghsl-2025-082.yaml
# Apply the fix
sisakulint -fix on script/actions/ghsl/ghsl-2025-082.yaml
The auto-fix will:
- Remove the unsafe
refparameter from the checkout step - This prevents checking out untrusted PR code
Test Files #
- Vulnerable pattern:
script/actions/ghsl/ghsl-2025-082.yaml