Fix self-update installing unverified .ps1 payloads - #92
Merged
Conversation
GitHub rewrites whitespace to dots in release asset names, so the monolithic published as "RackStack v1.2.3.ps1" is served as "RackStack.v1.2.3.ps1" while the SHA-256 manifest in the release body still lists the original spaced filename. Install-ScriptUpdate resolved the expected hash by regex-escaping $asset.name and matching it against the body, so on the .ps1 path the lookup could never match. $expectedHash came back null, and the code warned "skipping verification" and installed anyway - copying an unverified download over the running script and relaunching it elevated. The exact-name lookup for the .ps1 asset was dead code for the same reason, always falling through to the wildcard, which is what hid it. Updating the .exe was unaffected: "RackStack.exe" has no whitespace to rewrite, so its manifest lookup matched and its staged replace re-verified with certutil before the move. - Extract Get-ReleaseAssetHash, which normalizes space/dot spellings on both sides so either form resolves to the other - Normalize the same way when selecting the asset, so the exact match is no longer dead code - Fail closed: an update whose hash cannot be found is refused rather than installed with a warning Adds Run-Tests section 204, which exercises the real function against fixtures shaped like live GitHub API responses. Verified against the pre-fix logic: it returns null for the dotted asset, so the regression is genuinely caught rather than asserted. Also documents antivirus false positives, which are expected given an unsigned ps2exe binary that manages Defender exclusions. The new guide covers verifying a release via hash, cosign, and SLSA provenance, distinguishing a false positive from a tampered file, restoring from quarantine, and running the .ps1 to avoid the packed binary entirely.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug
GitHub rewrites whitespace to dots in release asset names, so the monolithic published as
RackStack v1.2.3.ps1is served asRackStack.v1.2.3.ps1— while the SHA-256 manifest in the release body still lists the original spaced filename.Install-ScriptUpdateresolved the expected hash by regex-escaping$asset.nameand matching it against the body. On the.ps1path that lookup could never match, so$expectedHashcame backnulland the code printedSHA256 hash not found in release notes — skipping verificationand installed anyway — copying an unverified download over the running script and relaunching it with-Verb RunAs.The exact-name lookup for the
.ps1asset was dead code for the same reason: it always fell through to theRackStack*.ps1wildcard, which is what hid the problem. Functionally the updater worked, so nothing looked wrong.Updating the
.exewas not affected —RackStack.exehas no whitespace to rewrite, so its manifest lookup matched and its staged replace re-verified withcertutilbefore the move.Not remotely exploitable on its own; it required a TLS MITM against
objects.githubusercontent.comor a compromised release. But that is exactly the threat the hash check exists to backstop, andSECURITY.mdadvertises it as an integrity guarantee.The fix
Get-ReleaseAssetHash, a pure function that normalizes space/dot spellings on both sides so either spelling resolves to the other.Verification
New
Run-Testssection 204 (15 assertions) exercises the realGet-ReleaseAssetHashagainst fixtures shaped like live GitHub API responses — dotted asset name, spaced manifest entry — rather than re-implementing the logic in the test.Mutation-verified rather than assumed: running the fixtures through the pre-fix logic returns
<null>for the dotted.ps1asset (so the regression is genuinely caught) while the EXE path still resolves correctly (confirming the EXE was never affected).Antivirus documentation
Separately,
RackStack.exeis periodically flagged by ML antivirus engines — most recently aBehavior:Win32/DefenseEvasion.A!mlquarantine on a user's machine and 12/65 on VirusTotal. These are false positives, and inherent to the build shape: an unsigned, ps2exe-packed binary that manages Defender exclusions is behaviourally indistinguishable from a dropper to a classifier.New
docs/Antivirus-Detections.mdcovers why it happens, how to prove a binary is the genuine published build (SHA-256, cosign, SLSA provenance), how to tell a false positive from a tampered file, restoring from quarantine, and running the.ps1to avoid the packed binary entirely.README.md,SECURITY.md, and the troubleshooting guide link to it;SECURITY.mdalso routes AV reports away from the vulnerability channel while naming the one condition that would make it a real security report.Release note
This bumps the version to v1.122.1 (patch — bug fix), so merging will cut a release.