Skip to content

Supply chain: release binaries are verified against a checksum file from the same mutable release #146

Description

@eiserv

What

scripts/prepare-action.sh downloads two files from the same GitHub Release and verifies one against the other:

download_release_file "$version" 'checksums.txt' "$checksums" 1048576
download_release_file "$version" "$asset" "$binary" 104857600
verify_release_checksum "$binary" "$checksums" "$asset"

Both come from https://github.com/eiserv/easySFTP/releases/download/$version/... (scripts/action-lib.sh, download_release_file). The SHA-256 check therefore proves only that the binary was not corrupted in transit. Anyone who can write release assets can replace the binary and checksums.txt together, and the check still passes.

Why it matters

This breaks the guarantee users think they are buying when they pin the action:

  • uses: eiserv/easySFTP@<40-char-sha> pins the launcher (the checked-out scripts/, action.yml, .easysftp-version). It does not pin the binary that actually reads their SSH private key and connects to their server. That binary is fetched at run time from a release whose assets are mutable.
  • Release assets really are mutable in this repo by design: .github/workflows/release-binaries.yml uploads with gh release upload "$TAG" release/* --clobber, and .github/workflows/repair-release.yml exists specifically to re-run that upload for an already published tag.
  • docs/security.md currently reads "Exact version tags (v3.0.0) are immutable once published". That is true of the git tag and not true of the release asset the launcher downloads, so the doc oversells the current guarantee.

An easySFTP run holds the deploy credentials for the user's production server, so this is the highest-value target in the whole project.

Suggested directions (pick one or both)

  1. Commit the expected hashes into the repo at the release commit. A file such as checksums.txt (or per-platform lines in .easysftp-version's neighbourhood) that release-binaries.yml writes back to the tagged tree, and that verify_release_checksum reads from $action_path instead of downloading. Then a pinned action ref genuinely pins the binary, because the expected hash travels with the checkout. This is the smallest change with the biggest gain.
  2. Sign the release assets with cosign / Sigstore keyless signing (GitHub OIDC) in release-binaries.yml, and verify in the launcher with cosign verify-blob --certificate-identity-regexp ... --certificate-oidc-issuer https://token.actions.githubusercontent.com. This costs a cosign install step on the runner, so it is the heavier option; it does protect users pinned to the rolling @v3 tag, which option 1 alone does not.

Whichever is chosen, docs/security.md and SECURITY.md should be updated to state precisely what is and is not pinned.

Notes

  • Also worth checking while in this code: curl --max-filesize only aborts early when the server sends a Content-Length. It is a useful guard, not a hard cap.

Metadata

Metadata

Assignees

No one assigned

    Labels

    securitySecurity-relevant change or hardening

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions