Skip to content

SHA-pinned actions still resolve mutable executable bytes in two reusables #173

Description

@rldyourmnd

Found while auditing macos-ubuntu-bootstrap's consumption of these reusables. Filing here because the cause is in this repository, not in the consumer.

Both findings are the same class: an action pinned by commit SHA does not pin what that action executes. The repository's action-pinning discipline stops at the wrapper's metadata and does not follow it to the bytes.


1. scorecard-action executes a tag-addressed container image

.github/workflows/public-scorecard-json.yml:52 pins the action correctly:

uses: ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc  # v2.4.4

That commit's action.yaml ends:

runs:
  using: "docker"
  image: "docker://ghcr.io/ossf/scorecard-action:v2.4.4"

The image is addressed by tag. Retagging or replacing ghcr.io/ossf/scorecard-action:v2.4.4 changes the container every consumer of this reusable executes, with no change to any pinned SHA and nothing in any consumer's diff to review.

Verified at the pinned commit:

$ gh api repos/ossf/scorecard-action/git/trees/2d1146689b8cda280b9bc96326124645441f03bc \
    --jq '.tree[]|select(.path|test("^action"))'
action.yaml  048b824172dabec256108eeab49ea7e69cd08a39
# ... runs.image == "docker://ghcr.io/ossf/scorecard-action:v2.4.4"

No compromise observed. The finding is that the guarantee the SHA pin appears to give is not the guarantee it gives.

Suggested fix: pin the image by digest (docker://ghcr.io/ossf/scorecard-action@sha256:…) via a thin local wrapper, or invoke a checksum-verified binary the way osv-scan.yml and actionlint.yml already do in this repository — both of those are exemplary and are the reason this one stands out.

Proof it worked: a fixture that walks every pinned action's runs: metadata and fails on docker://…:tag or on a nested uses: resolving a floating version. Reverting the digest pin must fail it.

2. id-token: write is granted where publication is off

Same file:

permissions:
  id-token: write         # OIDC token for the Scorecard publish step

with publish_results an input that consumers can set to false. When it is false, the job holds a write-scoped OIDC capability it does not use. Least privilege here is cheap: make the permission conditional on the input, or document the runtime evidence that it is needed regardless.

I did not observe the runtime token request, so this half is unverified — if the action requests the token unconditionally, say so and close it.


For context on what the consumer did: macos-ubuntu-bootstrap had the same class of defect in its own ci.yml (raven-actions/actionlint pinned by SHA, version input defaulting to latest, no checksum) and fixed it by calling this repository's checksum-bound actionlint.yml instead — NDDev-it-com/macos-ubuntu-bootstrap#83. These two are what remain on this side of the boundary.

Metadata

Metadata

Assignees

No one assigned

    Labels

    ciContinuous integrationdependenciesDependency updates

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions