GHSL-2025-082: Cache Poisoning via Local Action Execution

GHSL-2025-082: Cache Poisoning via Local Action Execution #

Summary #

ItemValue
Advisory IDGHSL-2025-082
SeverityCritical
Affected Componentag-grid/ag-grid
CVEN/A
CWECWE-829 (Inclusion of Functionality from Untrusted Control Sphere)
Referencehttps://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:

  1. A workflow is triggered by a privileged event (issue_comment, pull_request_target, workflow_run)
  2. The workflow checks out untrusted PR code using ref: ${{ github.event.pull_request.head.sha }}
  3. 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 #

  1. Untrusted Checkout: ref: ${{ github.event.pull_request.head.sha }} checks out the PR author’s code
  2. Local Action Execution: ./.github/actions/setup-nx is read from the checked-out code
  3. Attacker Control: The attacker can replace the local action with arbitrary malicious code
  4. 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:

ConditionDescription
Unsafe Triggerissue_comment, pull_request_target, or workflow_run
Unsafe Checkoutref contains github.event.pull_request.head.sha/ref
Poisonable StepLocal action (./), local script, or build command after checkout

Poisonable Step Types #

TypePatternExample
Local Actionuses: ./..../.github/actions/setup-nx
Local Script./script.sh./scripts/setup.sh
Build Commandnpm install, make, etc.npm install, pip install
GitHub Script Importrequire('./local')require('./lib/helper')

Remediation #

- 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:

  1. First workflow: Triggered by pull_request, runs untrusted code
  2. 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:

  1. Remove the unsafe ref parameter from the checkout step
  2. This prevents checking out untrusted PR code

Test Files #

  • Vulnerable pattern: script/actions/ghsl/ghsl-2025-082.yaml

References #