Allow cask runs to access the network

Structured cask install steps run in a sandbox that denies network
access. This breaks casks whose upstream setup commands must retrieve
installation-time dependencies, including `gcloud-cli` virtualenv
creation.

- Add a serialised `network_access:` option to `run` steps.
- Omit the option from API data when it retains the default denial.
- Enable network access only when an explicit run requests it.
- Preserve network denial for existing steps and generated completions.
- Document the opt-in and cover both sandbox policies in tests.

This avoids disabling network restrictions for all cask operations while
giving affected casks an explicit, auditable escape hatch.

See #23495.
This commit is contained in:
Mike McQuaid
2026-08-11 12:02:16 +01:00
parent 5d49126d14
commit 8b3a119e78
5 changed files with 34 additions and 8 deletions
@@ -201,13 +201,13 @@ module Cask
cask.config
end
sig { returns(T.nilable(Sandbox)) }
def cask_sandbox
sig { params(network_access_allowed: T::Boolean).returns(T.nilable(Sandbox)) }
def cask_sandbox(network_access_allowed: false)
return unless Sandbox.use_for?("running cask artifact operations")
Sandbox.new.tap do |sandbox|
sandbox.allow_read(path: cask.staged_path, type: :subpath)
sandbox.add_install_hook_rules(network_access_allowed: false)
sandbox.add_install_hook_rules(network_access_allowed:)
end
end
@@ -33,7 +33,9 @@ module Cask
sig { params(command: T.class_of(SystemCommand), phase: Symbol).void }
def run_steps(command, phase: :install)
runner = Homebrew::InstallSteps::Runner.new(context: cask, command:)
sandbox = cask_sandbox
sandbox = cask_sandbox(network_access_allowed: steps.any? do |step|
step["type"] == "run" && step["network_access"] == true
end)
unless sandbox
runner.run(steps, phase:)
return
+4 -2
View File
@@ -683,11 +683,12 @@ module Homebrew
chdir: ::T.nilable(::T.any(::String, ::Pathname)),
writable_paths: Paths,
writable_base: ::T.nilable(::T.any(::String, ::Symbol)),
network_access: ::T::Boolean,
).void
}
def run(command, args: [], base: nil, env: {}, sudo: false, must_succeed: true, print_stdout: false,
print_stderr: true, stdin_path: nil, stdout_path: nil, chdir: nil, writable_paths: [],
writable_base: nil)
writable_base: nil, network_access: false)
add_step("run",
"command" => path_spec(command, base:, default_base: nil),
"args" => args.map(&:to_s),
@@ -703,7 +704,8 @@ module Homebrew
writable_paths,
base: writable_base,
default_base: @default_base,
))
),
"network_access" => network_access)
end
sig {
@@ -64,7 +64,7 @@ RSpec.describe Cask::Artifact::AbstractInstallSteps, :cask do
artifact = cask.artifacts.find { |candidate| candidate.is_a?(Cask::Artifact::PostflightSteps) }
run_step = artifact.steps.find { |step| step["type"] == "run" }
expect(run_step).not_to include("print_stdout", "suppress_stderr", "writable_paths")
expect(run_step).not_to include("print_stdout", "suppress_stderr", "writable_paths", "network_access")
end
it "sandboxes complete step blocks, including system commands" do
@@ -119,6 +119,28 @@ RSpec.describe Cask::Artifact::AbstractInstallSteps, :cask do
expect(original_home/"Library/Application Support/cask-home-state").to exist
end
it "allows network access for runs that request it" do
cask = Cask::Cask.new("with-networked-install-step") do
version "1.2.3"
sha256 :no_check
url "file://#{TEST_FIXTURE_DIR}/cask/container.zip"
postflight_steps do
run "/usr/bin/true", network_access: true
end
end
sandbox = instance_double(Sandbox).as_null_object
cask.staged_path.mkpath
cask.config_path.dirname.mkpath
allow(Sandbox).to receive_messages(available?: true, new: sandbox)
allow(Sandbox).to receive(:with_preserved_brew_file).and_yield
expect(sandbox).to receive(:add_install_hook_rules).with(network_access_allowed: true)
expect(sandbox).to receive(:run)
Cask::Installer.new(cask, command: NeverSudoSystemCommand).install_artifacts
end
context "when install steps may require sudo" do
{
"an explicitly privileged command" => proc { run "/usr/bin/true", sudo: true },
+1 -1
View File
@@ -659,7 +659,7 @@ Relative paths default to `staged_path` for `base:`, `source_base:` and `target_
Use `if_path_exists`, `unless_path_exists`, `on_macos` and `on_linux` blocks to guard one or more steps. A condition is evaluated once when its scope begins, so related steps make the same decision. Use `unless_path_exists` around `write_file` when an existing file must be preserved.
`run` does not evaluate a shell command string. It supports a literal `env:`, `stdin_path:`, `stdout_path:`, `chdir:` and `sudo:`. Standard output is hidden by default and standard error is printed; use `print_stdout: true` or `print_stderr: false` to change that behaviour. Failure aborts the installation or uninstallation; pass `must_succeed: false` when a non-zero exit status is expected and should be ignored, such as a cleanup command that cannot run when its dependency is absent. Nothing is written to `stdout_path:` when an ignored command fails. The complete steps block runs in an isolated Ruby subprocess without network or general home-directory access where Homebrew has an available sandbox. The sandbox permits writes to declared step destinations, the caskroom, app directory, temporary and cache directories and Homebrew's link directories. Ruby file operations and system or cask-provided commands therefore share the same restrictions. Use `writable_paths:` with directory roots, and `writable_base:` for relative roots, when an opaque command needs another declared write location.
`run` does not evaluate a shell command string. It supports a literal `env:`, `stdin_path:`, `stdout_path:`, `chdir:` and `sudo:`. Standard output is hidden by default and standard error is printed; use `print_stdout: true` or `print_stderr: false` to change that behaviour. Failure aborts the installation or uninstallation; pass `must_succeed: false` when a non-zero exit status is expected and should be ignored, such as a cleanup command that cannot run when its dependency is absent. Nothing is written to `stdout_path:` when an ignored command fails. The complete steps block runs in an isolated Ruby subprocess without network or general home-directory access where Homebrew has an available sandbox. Pass `network_access: true` when a `run` command must access the network. The sandbox permits writes to declared step destinations, the caskroom, app directory, temporary and cache directories and Homebrew's link directories. Ruby file operations and system or cask-provided commands therefore share the same restrictions. Use `writable_paths:` with directory roots, and `writable_base:` for relative roots, when an opaque command needs another declared write location.
#### Interpolation in steps blocks