Composite action should pin the actions it uses by commit SHA

ID

action_unpinned_uses

Severity

low

Remediation Complexity

trivial

Remediation Risk

low

Remediation Effort

low

Family

CI/ CD Security

Tags

ASVS50:v13.1.1, ASVS50:v15.2.1, attack-T1195.001, cicd-security, cwe-829, security, spvs10-v2.8.2, supply-chain

Description

A composite GitHub Custom Action (runs.using: composite) whose own steps uses: a cross-repository action through a floating reference — a tag, a branch, or no reference at all — instead of pinning it to a 40-character commit SHA.

Local (./path) and Docker (docker://) references are not flagged: the former is first-party code that is scanned anyway, the latter is covered by action_mutable_docker_image.

This complements pipeline_external_dependencies, which checks the consuming workflow. This detector checks the action definition itself, where an unpinned dependency is easy to miss — a workflow author who diligently pins every uses: still inherits whatever the actions they call have left floating.

Security

A tag or branch is a mutable pointer: the upstream owner (or anyone who compromises that account) can repoint it at different code, and every consumer picks up the change on the next run without a diff to review. This is the tj-actions/changed-files class of supply-chain attack, where a widely used action’s tags were rewritten to exfiltrate CI secrets.

The blast radius is larger for an action than for a workflow. A composite action runs inside the caller’s job, with the caller’s secrets, token and workspace — so a substituted dependency reaches every repository that consumes the action, transitively, through a reference the consumer never wrote.

Floating references also break reproducibility: two runs of the same pipeline can execute different code with no change on the consumer’s side.

Mitigation / Fix

Pin every cross-repository uses: to a full 40-character commit SHA, keeping the human-readable version in a trailing comment:

runs:
  using: composite
  steps:
    # Pinned — immutable:
    - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
    # Avoid — mutable tag, can be repointed at malicious code:
    # - uses: actions/checkout@v6
    # Avoid — branch reference, changes on every upstream push:
    # - uses: actions/checkout@main

A short SHA is not sufficient: pin the full 40 characters, since abbreviated hashes can collide and are accepted loosely by some tooling.

Update pins deliberately — review the upstream diff when moving to a new SHA — and let Dependabot (package-ecosystem: github-actions) raise the bumps so pinning does not mean going stale.