GHSL-2024-325: Arbitrary Code Execution via Untrusted Fork Checkout

GHSL-2024-325: Arbitrary Code Execution via Untrusted Fork Checkout #

Summary #

ItemValue
Advisory IDGHSL-2024-325
SeverityCritical
Affected ComponentActual (visual regression testing tool)
CVEN/A
CWECWE-94 (Improper Control of Generation of Code)
Referencehttps://securitylab.github.com/advisories/GHSL-2024-325_GHSL-2024-326_Actual/

Vulnerability Description #

GHSL-2024-325 is an arbitrary code execution vulnerability that occurs when a GitHub Actions workflow:

  1. Runs on a privileged trigger (issue_comment, pull_request_target, etc.)
  2. Checks out code from an untrusted fork repository
  3. Executes local actions (./.github/actions/*) from that untrusted code

This allows an attacker to execute arbitrary code with the workflow’s elevated permissions by:

  1. Creating a malicious fork with a backdoored local action
  2. Submitting a pull request from that fork
  3. Triggering the workflow via comment (e.g., /update-vrt)

Attack Vector #

sequenceDiagram
    participant Attacker
    participant Fork
    participant MainRepo
    participant Workflow

    Attacker->>Fork: Create fork with malicious .github/actions/setup
    Attacker->>MainRepo: Open PR from fork
    Attacker->>MainRepo: Comment "/update-vrt"
    MainRepo->>Workflow: Trigger issue_comment workflow
    Workflow->>Fork: Checkout attacker's code
    Workflow->>Workflow: Execute ./.github/actions/setup (MALICIOUS)
    Workflow-->>Attacker: Secrets exfiltrated, repo compromised

Vulnerable Code Pattern #

name: Update VRT (Vulnerable)

on:
  issue_comment:
    types: [created]

jobs:
  update-vrt:
    if: github.event.issue.pull_request && contains(github.event.comment.body, '/update-vrt')
    runs-on: ubuntu-latest
    permissions:
      contents: write
      pull-requests: write
    steps:
      - name: Get PR branch info
        id: comment-branch
        uses: actions/github-script@v7
        with:
          script: |
            const pr = await github.rest.pulls.get({...});
            core.setOutput('head_owner', pr.data.head.repo.owner.login);
            core.setOutput('head_repo', pr.data.head.repo.name);
            core.setOutput('head_ref', pr.data.head.ref);

      # VULNERABLE: Checkout from untrusted fork
      - name: Checkout PR code
        uses: actions/checkout@v4
        with:
          repository: ${{ steps.comment-branch.outputs.head_owner }}/${{ steps.comment-branch.outputs.head_repo }}
          ref: ${{ steps.comment-branch.outputs.head_ref }}

      # VULNERABLE: Execute local action from untrusted code
      - name: Set up environment
        uses: ./.github/actions/setup

sisakulint Detection #

sisakulint detects this vulnerability pattern with the following rules:

1. cache-poisoning-poisonable-step #

script/actions/ghsl-2024-325-326.yaml:39:9: cache poisoning risk via local action:
'Set up environment' runs untrusted code after checking out PR head (triggers: issue_comment).
Attacker can steal cache tokens [cache-poisoning-poisonable-step]

This rule identifies when:

  • A workflow uses privileged triggers (issue_comment, pull_request_target, workflow_run)
  • Untrusted code is checked out (from fork or PR head)
  • Local actions are executed after the checkout
RuleDescription
dangerous-triggers-criticalIdentifies privileged triggers without security mitigations
commit-shaWarns about unpinned action versions
artipackedDetects credential persistence in checkout

Remediation #

Option 1: Checkout from base repository only #

- uses: actions/checkout@v4
  with:
    ref: ${{ github.event.pull_request.base.ref }}

Option 2: Use workflow_call with restricted permissions #

# In main workflow
jobs:
  build:
    uses: ./.github/workflows/safe-build.yml
    with:
      pr-number: ${{ github.event.issue.number }}

Option 3: Avoid local action execution from untrusted code #

# Use pinned external actions instead of local actions
- uses: owner/setup-action@v1.0.0

Test Files #

  • Vulnerable pattern: script/actions/ghsl/ghsl-2024-325-326.yaml

References #