Executive Summary
In gouef/githubtoplanguages, the scheduled workflow top-languages.yml invoked its own composite action using a mutable branch reference (uses: gouef/githubtoplanguages@main) while passing secrets.USER_GITHUB_TOKEN (a long-lived classic PAT with write privileges) into the environment. Because GitHub Actions resolves mutable branch refs to the latest branch head on each run, any merged change to action.yml automatically executes with the PAT's account-level privileges on scheduled cron runs, requiring no workflow modification or review approval.
Coordinated Disclosure Timeline
Identified self-referential mutable composite action pattern with injected classic PAT.
Executed controlled replication PoC (runner-selfref-poc) proving weaponized action execution and Gist creation via injected PAT.
Conducted live on-target probe (PR #6) confirming fork-PR approval boundaries and establishing the insider/merge attack vector.
Reported privately to maintainer (@JanGalek) via GitHub Security Advisories; accepted and CVE requested.
Official GitHub Security Advisory GHSA-x3cj-mm38-329g published on GitHub Advisory Database as Critical severity.
01.The Self-Referential Pattern (.github/workflows/top-languages.yml)
The repository's core automation workflow runs on a daily schedule (0 0 1 * *) and on pushes to main. Instead of referencing local steps or a pinned commit SHA, it calls its own action via a mutable remote branch reference:
jobs:
build-and-run:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Run custom action
uses: gouef/githubtoplanguages@main # Self-reference to this repo's mutable main
env:
GITHUB_TOKEN: ${{ secrets.USER_GITHUB_TOKEN }} # Long-lived classic PAT02.The Root Cause Chain & Token-Level Persistence
GitHub Actions downloads composite actions fresh on every invocation, resolving @main to the current head commit. Because action.yml uses runs: { using: composite } with arbitrary bash steps (git config, go build, ./app, git push), whoever controls the content of action.yml controls arbitrary code execution on the runner. Passing USER_GITHUB_TOKEN into every step grants account-level write access that outlives the ephemeral runner VM until revoked.
Reproduction & Proof of Concept (PoC)
Validation was conducted through both a controlled lab environment and live on-target assessment.
- Controlled PoC Setup: Created runner-selfref-poc replicating top-languages.yml with uses: madhanalagarsamy/runner-selfref-poc@main and injected test PAT.
- Baseline verification: Confirmed action executed from main at SHA c38501c with token authenticated as owner.
- Weaponization: Merged a malicious curl step into action.yml only (zero edits to .github/workflows/*).
- Re-run execution: The scheduled workflow automatically pulled updated SHA 53d2f02; the malicious step ran and successfully executed a privileged write API call (created private Gist ddbb9dac...).
- Live on-target validation (PR #6): Probed fork-PR behavior; confirmed GitHub's first-time contributor approval gate blocks external unapproved execution, proving the primary attack vector is insider merge or maintainer account compromise.
- name: MALICIOUS STEP (merged via action.yml@main)
shell: bash
run: |
echo "=== ATTACKER CODE EXECUTED ON NEXT TRIGGERED RUN ==="
curl -s -X POST \
-H "Authorization: Bearer $GITHUB_TOKEN" \
-H "Accept: application/vnd.github+json" \
https://api.github.com/gists \
-d '{"description":"poc","public":false,"files":{"pwn.txt":{"content":"pwned"}}}'Impact & Blast Radius
Remediation & Mitigation
steps:
- uses: actions/checkout@v4
# Option A: Local action reference
- name: Run hardened local action
uses: ./ # uses local checkout rather than pulling mutable @main
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # ephemeral repository token