Skip to content

Fix self-update installing unverified .ps1 payloads - #92

Merged
TheAbider merged 3 commits into
masterfrom
fix/selfupdate-hash-verification
Jul 28, 2026
Merged

Fix self-update installing unverified .ps1 payloads#92
TheAbider merged 3 commits into
masterfrom
fix/selfupdate-hash-verification

Conversation

@TheAbider

Copy link
Copy Markdown
Owner

The bug

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. On the .ps1 path that lookup could never match, so $expectedHash came back null and the code printed SHA256 hash not found in release notes — skipping verification and installed anyway — copying an unverified download over the running script and relaunching it with -Verb RunAs.

The exact-name lookup for the .ps1 asset was dead code for the same reason: it always fell through to the RackStack*.ps1 wildcard, which is what hid the problem. Functionally the updater worked, so nothing looked wrong.

Updating the .exe was not affected — RackStack.exe has no whitespace to rewrite, so its manifest lookup matched and its staged replace re-verified with certutil before the move.

Not remotely exploitable on its own; it required a TLS MITM against objects.githubusercontent.com or a compromised release. But that is exactly the threat the hash check exists to backstop, and SECURITY.md advertises it as an integrity guarantee.

The fix

  • Extract Get-ReleaseAssetHash, a pure function that normalizes space/dot spellings on both sides so either spelling 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 resolved is now refused, not installed with a warning. This function executes its payload with elevation; an unverifiable update must not proceed.

Verification

New Run-Tests section 204 (15 assertions) exercises the real Get-ReleaseAssetHash against 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 .ps1 asset (so the regression is genuinely caught) while the EXE path still resolves correctly (confirming the EXE was never affected).

  • 5417/5417 structural tests pass, 0 failures
  • PSScriptAnalyzer: 0 findings on both changed files
  • Monolithic sync clean, parse check passed, UTF-8 BOMs intact

Antivirus documentation

Separately, RackStack.exe is periodically flagged by ML antivirus engines — most recently a Behavior:Win32/DefenseEvasion.A!ml quarantine 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.md covers 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 .ps1 to avoid the packed binary entirely. README.md, SECURITY.md, and the troubleshooting guide link to it; SECURITY.md also 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.

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

codecov Bot commented Jul 28, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@TheAbider
TheAbider merged commit 4a2afbe into master Jul 28, 2026
6 checks passed
@TheAbider
TheAbider deleted the fix/selfupdate-hash-verification branch July 28, 2026 10:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant