Trusted Third-Party Tap Supply Chain
Prerequisites
- Victim installs an attacker-controlled fully qualified item, explicitly trusts that item, or trusts the whole tap
- Homebrew later loads the trusted formula, cask, or command; `brew update` alone can fetch changes but does not by itself grant trust
- For typosquat variants: a name close enough to a legitimate tap to trick users (e.g. `hashicorp/tap` vs `hashi-corp/tap`)
Attack Scenarios
Typosquatted Tap Mimicking a Popular Vendor
An attacker registers a GitHub repository named `homebrew-
# Attacker owns github.com/hashi-corp (note the hyphen)
mkdir homebrew-tap && cd homebrew-tap
mkdir Formula
cat > Formula/terraform.rb <<'RUBY'
class Terraform < Formula
desc "Infrastructure as code tool"
homepage "https://www.terraform.io/"
url "https://releases.hashicorp.com/terraform/1.8.0/terraform_1.8.0_darwin_arm64.zip"
# Illustrative digest; a real formula must match its source archive.
sha256 "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
# Formula-body Ruby runs when Homebrew loads this trusted formula.
File.write("/tmp/terraform-formula-loaded", Time.now.to_s)
def install
bin.install "terraform"
end
end
RUBY
git init && git add . && git commit -m "Initial tap"
git remote add origin git@github.com:hashi-corp/homebrew-tap.git
git push -u origin main
# Copy-pasted from a poisoned tutorial
brew install hashi-corp/tap/terraform
# The fully qualified command trusts only this formula, then loads it.
Compromised Tap Pushing a Silent Update
An attacker who compromises an existing tap can modify a formula that users already trust. `brew update` can fetch the changed repository, and a later operation that loads or upgrades the trusted formula can evaluate the new Ruby. Trust is attached to the item or tap rather than to a reviewed commit, so users must separately review updates when that distinction matters.
[1][2][3][4][5]# Single-commit diff to an existing, trusted formula in the tap
diff --git a/Formula/widget.rb b/Formula/widget.rb
--- a/Formula/widget.rb
+++ b/Formula/widget.rb
@@ class Widget < Formula
+ File.write("/tmp/widget-formula-loaded", Time.now.to_s)
+
def install
bin.install "widget"
end
brew update
# Fetches the changed tap; the update itself is not the execution step.
brew upgrade widget
# Loading the already-trusted formula evaluates the changed Ruby.
Overriding a Core Formula From an Attacker Tap
If a user installs a formula by its fully qualified tap path (`attacker/tap/jq`), Homebrew selects that item even if `homebrew/core` also has `jq`; the command also trusts that individual third-party formula. The unqualified command `brew install jq` continues to select the core formula, so the attacker must persuade the user to choose the hostile namespace.
[1][2][3][4][5]# Formula/jq.rb in attacker/homebrew-utils
class Jq < Formula
desc "Command-line JSON processor (faster build)"
url "https://example.com/jq-1.7.tar.gz"
sha256 "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
def install
bin.install "jq"
end
end
brew tap attacker/utils
brew install attacker/utils/jq
Detection
Enumerate all tapped repositories and verify their origins
Every tap is a git clone under `$(brew --repository)/Library/Taps`. List taps, trust entries, and remotes, then confirm each resolves to the intended organization. Unexpected taps or broader-than-needed trust entries warrant review.
[1][2][3][4][5]brew tap
brew trust
brew untrust
for t in $(brew tap); do
echo "=== $t ==="
git -C "$(brew --repository)/Library/Taps/${t/\//\/homebrew-}" remote -v
done
Review tap git history before upgrading
Diff a trusted tap's recent commits before running an upgrade when you need commit-level review. Homebrew's item and tap trust entries do not attest to a specific repository revision.
[1][2][3][4][5]export HOMEBREW_NO_AUTO_UPDATE=1
cd "$(brew --repository)/Library/Taps/attacker/homebrew-tap"
git fetch origin
git log --oneline HEAD..origin/main
git diff HEAD..origin/main -- Formula/ Casks/
Block unknown taps at the endpoint or proxy layer
For managed macOS fleets, restrict outbound git traffic so `brew tap` can only clone from an allow-listed set of GitHub orgs (homebrew/, the vendor taps your org actually uses). This prevents typo-squats from resolving.
[1][2][3][4][5]# Example: list taps present across a fleet via MDM / osquery
osqueryi --json "SELECT path FROM file WHERE path LIKE '/opt/homebrew/Library/Taps/%/homebrew-%' AND type = 'directory';"
Mitigation
- Prefer item-level trust (`brew trust --formula`, `--cask`, or `--command`) and grant whole-tap trust only when every current and future item is acceptable
- Before installing, verify the organization and fully qualified item name against the vendor's official documentation
- Set `HOMEBREW_NO_AUTO_UPDATE=1` in CI and dev shells so tap updates do not land without explicit review
- Pin critical taps to a reviewed commit with `git -C $(brew --repository)/Library/Taps/
checkout ` - Periodically audit `brew tap` output and remove taps the team no longer uses (`brew untap`)
- Restrict cloning scope at the network layer for managed fleets to approved GitHub organizations
- Prefer the fully-qualified form (`homebrew/core/jq`) when scripting installs, so a tap cannot shadow a core formula