How to check Homebrew packages for CVEs before you upgrade
It is Monday morning, coffee in hand, and you run brew upgrade to catch your tools up — gh, ImageMagick, a
database client, whatever drifted out of date over the weekend. A list scrolls past, you press enter, and Homebrew
cheerfully downloads and installs the newest version of each one. It is probably the most trusted command in your
day: Homebrew is where your Mac’s developer tools come from, so of course you keep it current. Updating is the
responsible thing to do, right?
Here is the assumption hiding inside that habit. brew upgrade never asks whether the new version it is about to
install carries a known security flaw — or whether the recipe describing it was quietly changed. It simply fetches
the latest and runs it. Homebrew’s packages are not screened for security: they are formulae maintained by
volunteers in public taps, and a single compromised formula, or a hijacked tap, reaches every machine that upgrades
from it. Most mornings that is completely fine. The one morning it is not, you have handed a fresh, hostile binary
the run of your laptop — and you did it deliberately, with the command you trust most.
It is also rarely just the one tool you meant to update. Bumping a single formula can pull in or upgrade a handful
of dependencies, each its own download from its own maintainer, and brew upgrade checks none of them against any
vulnerability database — that is simply not what it is for. homebrew-safe-upgrade makes it what it is for: before
anything is installed or upgraded, it checks the target version of every package, and the dependencies arriving with
it, and only lets the clean ones through.
The problem, in plain terms
There are really three separate ways a brew upgrade can hurt you, and stock Homebrew guards against none of them.
The obvious one is a known vulnerability: the new version has a published CVE, and you install it anyway because
nothing stopped to look. The second is freshness: the most dangerous release is often the one published an hour
ago, in the gap between an attacker stealing a maintainer’s credentials and the world noticing — far too new for any
CVE database to list. The third is tampering: a tap that has been hijacked can serve a different binary under a
version number you already trust, so the version string looks normal while the bytes are not.
Who runs into this
Every developer on macOS or Linux who uses Homebrew — which is most of them — and every CI pipeline that
brew installs its toolchain. None of them reads the changelog of every transitive dependency before hitting enter;
the whole point of brew upgrade is that you do not have to think about it. That is exactly why the check belongs in
the command itself.
What homebrew-safe-upgrade does — and how it works
homebrew-safe-upgrade adds two commands, brew safe-upgrade and brew safe-install, that wrap the real thing.
Each one runs the same gate before handing off to Homebrew.
brew installs — vulnerable, too-fresh or tampered ones are held.It checks the target version against three databases
Every outdated package is checked against OSV.dev, the GitHub Advisory Database and NIST NVD, with version-aware
filtering so an old CVE that does not affect the version you are actually installing does not raise a false alarm.
Clean packages are offered for upgrade; vulnerable ones are blocked and listed separately. (--yes runs it
unattended for CI.)
It checks the dependencies coming in with the upgrade
The same gate is applied to the transitive dependencies the operation would bring onto your system — both brand-new ones and existing ones whose version is being bumped — so a clean-looking tool cannot smuggle a vulnerable dependency in behind it.
A freshness hold for brand-new releases
By default, any formula or cask published less than three days old is held back — the window in which a compromised
release is typically live but not yet flagged anywhere. The hold is fail-closed: if a package’s age cannot be
verified (the source repo is unreachable, GitHub is rate-limiting), it is held, not waved through. It is also
CVE-aware — if the version you currently have installed has a known flaw, the hold on its fix is skipped, so a
freshness rule never keeps a security patch from you. --min-age 0 turns it off.
A tamper check for hijacked taps
This is the defence against the version-string-looks-fine-but-the-bytes-changed attack. homebrew-safe-upgrade
compares each bottle’s SHA against the canonical hash published at formulae.brew.sh, and blocks a tap that ships a
different binary under the same version number before Homebrew ever installs it.
Install & use it
Install
Add the 5bats tap once, then install the wrapper:
brew install sharkyger/tap/safe-upgrade
Use it
brew safe-upgrade # check every outdated package, upgrade only the clean ones
brew safe-install wget curl # check these (and their deps) before installing
brew safe-upgrade --yes # unattended, for CI
brew safe-upgrade --min-age 7 # stricter freshness hold
Full flag reference, cask coverage notes, and source are on GitHub: github.com/sharkyger/homebrew-safe-upgrade (MIT).
FAQ
Are Homebrew packages vetted for security?
No. Homebrew formulae and casks are community-maintained recipes in public taps; nobody screens each release for
known vulnerabilities, and brew upgrade does not check. homebrew-safe-upgrade adds that check before anything
installs.
Does brew upgrade check for vulnerabilities?
It does not — brew upgrade fetches and installs the newest version regardless of known CVEs. brew safe-upgrade
checks each target version, and the dependencies coming in with it, against three vulnerability databases first, and
only upgrades the clean ones.
Can a Homebrew tap be tampered with?
Yes — a compromised tap could serve a different binary under a version number you already trust. homebrew-safe-upgrade compares each bottle’s SHA against Homebrew’s canonical record and blocks a mismatch before brew installs it.
homebrew-safe-upgrade is one of the 5bats supply-chain gates — the same check-before-it-runs idea applied to Python, PHP, and the packages an AI assistant installs.
