Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions Library/Homebrew/extend/os/mac/sandbox.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,19 @@ module Sandbox
(deny file-write*) ; deny non-allowlist file write operations
(deny file-write-setugid) ; deny non-allowlist file write SUID/SGID operations
(deny file-write-mode) ; deny non-allowlist file write mode operations
(deny mach-lookup)
(allow mach-lookup
(global-name "com.apple.bsd.dirhelper")
(global-name "com.apple.system.opendirectoryd.libinfo")
(global-name "com.apple.system.opendirectoryd.membership")
(global-name "com.apple.PowerManagement.control")
(global-name "com.apple.SecurityServer")
(global-name "com.apple.networkd")
(global-name "com.apple.ocspd")
(global-name "com.apple.trustd.agent")
(global-name "com.apple.SystemConfiguration.DNSConfiguration")
(global-name "com.apple.SystemConfiguration.configd")
)
(deny lsopen)
(deny appleevent-send)
(allow process-exec
Expand Down
54 changes: 52 additions & 2 deletions Library/Homebrew/test/sandbox_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
# frozen_string_literal: true

require "sandbox"
require "securerandom"

RSpec.describe Sandbox, :needs_macos do
subject(:sandbox) { described_class.new }
Expand All @@ -26,8 +27,8 @@
end.new
end

it "denies LaunchServices and Apple Events even when network access is allowed" do
expect(sandbox.seatbelt_profile).to include("(deny lsopen)", "(deny appleevent-send)")
it "restricts Mach services, LaunchServices and Apple Events even when network access is allowed" do
expect(sandbox.seatbelt_profile).to include("(deny mach-lookup)", "(deny lsopen)", "(deny appleevent-send)")
end
end

Expand Down Expand Up @@ -82,6 +83,25 @@
end

describe "#run" do
let(:handlers_for_scheme) do
lambda do |scheme|
SystemCommand.run!("/usr/bin/osascript", args: ["-l", "JavaScript", "-e", <<~JS, scheme]).stdout
ObjC.import('CoreServices');
function run(argv) {
return JSON.stringify(ObjC.deepUnwrap(ObjC.castRefToObject($.LSCopyAllHandlersForURLScheme($(argv[0])))) || []);
}
JS
end
end

it "reports an empty array for an unregistered URL scheme" do
expect(handlers_for_scheme.call("org.homebrew.sandbox-#{SecureRandom.uuid}")).to eq("[]\n")
end

it "reports registered HTTP URL handlers" do
expect(JSON.parse(handlers_for_scheme.call("http"))).not_to be_empty
end

it "prevents LaunchServices from launching an application outside the sandbox" do
app = dir/"SandboxTest.app"
SystemCommand.run!("/usr/bin/osacompile", args: ["-o", app, "-e", "return"])
Expand All @@ -91,6 +111,36 @@
expect { sandbox.run "/usr/bin/open", "-W", "-n", app }.to raise_error(ErrorDuringExecution)
end

it "prevents LaunchServices from registering a URL handler" do
lsregister = "/System/Library/Frameworks/CoreServices.framework/Frameworks/" \
"LaunchServices.framework/Support/lsregister"
identifier = "org.homebrew.sandbox-#{SecureRandom.uuid}"

# LaunchServices does not register applications under /private/tmp.
Dir.mktmpdir("homebrew-sandbox", "#{Dir.home(ENV.fetch("USER"))}/Library/Caches") do |cache|
app = Pathname(cache)/"SandboxTest.app"
SystemCommand.run!("/usr/bin/osacompile", args: ["-o", app, "-e", "return"])
SystemCommand.run!("/usr/bin/plutil", args: [
"-replace", "CFBundleIdentifier", "-string", identifier, app/"Contents/Info.plist"
])
SystemCommand.run!("/usr/bin/plutil", args: [
"-insert", "CFBundleURLTypes", "-json", [{ CFBundleURLSchemes: [identifier] }].to_json,
app/"Contents/Info.plist"
])
sandbox.allow_write_temp_and_cache
sandbox.allow_write_path(cache)

# lsregister's exit status does not indicate whether registration succeeded.
sandbox.run "/bin/sh", "-c", '"$@"; exit 0', "--", lsregister, "-f", app
expect(handlers_for_scheme.call(identifier)).to eq("[]\n")

SystemCommand.run!(lsregister, args: ["-f", app])
expect(handlers_for_scheme.call(identifier)).to include(identifier)
ensure
SystemCommand.run(lsregister, args: ["-u", app]) if app
end
end

it "fails when writing to file not specified with ##allow_write" do
expect do
sandbox.run "touch", file
Expand Down
3 changes: 2 additions & 1 deletion docs/Cask-Cookbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,8 @@ Official macOS casks must also meet the [Gatekeeper requirement](Acceptable-Cask

Generated completion artifacts are different: `generate_completions_from_executable` runs an installed executable only to produce shell completion text. The complete generation operation, including writing the completion, runs in an isolated Ruby subprocess where Homebrew has an available sandbox. The sandbox allows reading the staged cask, writing the completion and temporary/cache files and blocks network access. This limits side effects from commands that should only print completion data.

On macOS, sandboxed operations cannot launch applications through LaunchServices (including `open`) or send Apple Events to other applications.
On macOS, sandboxed operations can only look up explicitly allowed Mach services for directory information, power management, networking and certificates.
They cannot register or launch applications through LaunchServices (including `lsregister` and `open`) or send Apple Events to other applications.
These restrictions also apply to steps with `network_access: true`.

`installer script:` is not sandboxed. Many installer scripts are vendor installers that require broad filesystem writes, macOS services or `sudo`; macOS sandboxing does not work for root processes, and narrowing the write allowlist to the Caskroom plus uninstall or zap paths would break installers that legitimately write elsewhere. It would also change documented `SystemCommand` behaviours such as `sudo:`, `must_succeed:` and output handling.
Expand Down
Loading