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)
- 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.
- 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.
What
scripts/prepare-action.shdownloads two files from the same GitHub Release and verifies one against the other: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 andchecksums.txttogether, 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-outscripts/,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..github/workflows/release-binaries.ymluploads withgh release upload "$TAG" release/* --clobber, and.github/workflows/repair-release.ymlexists specifically to re-run that upload for an already published tag.docs/security.mdcurrently 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)
checksums.txt(or per-platform lines in.easysftp-version's neighbourhood) that release-binaries.yml writes back to the tagged tree, and thatverify_release_checksumreads from$action_pathinstead 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.release-binaries.yml, and verify in the launcher withcosign 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@v3tag, which option 1 alone does not.Whichever is chosen,
docs/security.mdandSECURITY.mdshould be updated to state precisely what is and is not pinned.Notes
curl --max-filesizeonly aborts early when the server sends aContent-Length. It is a useful guard, not a hard cap.