Sandboxing a third-party macOS app to restrict writing to one folder
App sandbox
This investigation began because I was using a commercial password manager program that had the ability to export an unencrypted text file of all my passwords. I wanted to be sure that when I used that feature, it would only be able to write the file to a specific directory of my choice.
The macOS App Sandbox is a facility that restricts an application's access to system resources and user data. Any developer that submits an application to the App Store must enable the App Sandbox and specify which entitlements are required by the app.
For example, entitlements control whether an app can use the microphone, access the network, read files in the Pictures or Music folders, etc.
This collection of entitlements is the official way to specify an app's capabilities. It is documented and supported by Apple.
Behind the scenes, there is a much more powerful and granular system of controlling an app's capabilities. It works by creating a profile file that specifies the allowed or forbidden operations in detail. It is not well documented, and assuredly not supported by Apple, but it is the system I needed.
As a long-time Apple DTS (Developer Tech Support) engineer wrote:
This system is invoked by using the command-line tool sandbox-exec
.
Documentation
The only non-folkloric documentation is found in the man page for sandbox-exec
:
SANDBOX-EXEC(1) BSD General Commands Manual SANDBOX-EXEC(1)
NAME
sandbox-exec -- execute within a sandbox (DEPRECATED)
SYNOPSIS
sandbox-exec [-f profile-file] [-n profile-name] [-p profile-string] [-D key=value ...] command [arguments ...]
DESCRIPTION
The sandbox-exec command is DEPRECATED. Developers who wish to sandbox an app should instead adopt the App Sandbox fea-
ture described in the App Sandbox Design Guide. The sandbox-exec command enters a sandbox using a profile specified by
the -f, -n, or -p option and executes command with arguments.
The options are as follows:
-f profile-file
Read the profile from the file named profile-file.
-n profile-name
Use the pre-defined profile profile-name.
-p profile-string
Specify the profile to be used on the command line.
-D key=value
Set the profile parameter key to value.
SEE ALSO
sandbox_init(3), sandbox(7), sandboxd(8)
Mac OS X March 9, 2017 Mac OS X
In order to make any progress, I had to refer to unofficial sources of information. Some of the more useful are listed below.
The first of these is an early effort at a comprehensive description of the sandbox facility; it is valuable although dated. The other links are reports of individuals who are sharing their experiences with the sandbox.
- Apple's Sandbox Guide [2011]
- macOS: How to run your Applications in a Mac OS X sandbox to enhance security [2015]
- A quick glance at macOS' sandbox-exec [2019]
- macOS: App sandboxing via sandbox-exec [2020]
- How to sandbox third party applications when `sandbox-exec` is deprecated now? [2020]
Sandbox documentation has been a moving target over the years. Because it is a private interface, Apple is under no obligation to maintain forward or backward compatibility. Take note of the publication date of any information found online.
First Example
To try out sandboxing, I first wrote a simple command-line program that just writes some text to a file at a specified location.
After some experimenting and confusion, it worked as advertised; I could specify a folder forbidden for writing (with all others allowed) or an allowed folder (with all others forbidden).
Note that this is independent of the familiar rwx
file permissions for owner, group, and world, which still apply.
For anyone who wants to see the details of the experimental results, they are documented below, otherwise skip ahead to Second Example.
If you want to use sandbox-exec
yourself, it may be a good idea to start with something like this as a basic sanity check
on whether it is working in whatever (future) version of macOS you are using.
Here's the test program:
// writefoo.c
// write a text file
#include
#include
// Compile & link:
// gcc -o writefoo writefoo.c
// Usage:
// writefoo [output path]
int main(int argc, const char * argv[]) {
// first argument is file path; if not specified, use "foo.txt"
const char *name = argc > 1 ? argv[1] : "foo.txt";
FILE *foo;
char *message = "Testing one, two, three."; // text to write into file
printf("Write %s\n", name);
foo = fopen(name, "w");
if (foo == NULL) {
printf("Failed file open\n");
} else {
size_t result = fwrite(message, strlen(message), 1, foo);
if (result == 1) {
printf("OK\n");
} else {
printf("Failed file write\n");
}
fclose(foo);
}
return 0;
}
I used the following profile files for testing:
allow-all.sb
(version 1)
(allow default)
Worked as expected:
$ sandbox-exec -f allow-all.sb ./writefoo /Volumes/Work/one.txt
Write /Volumes/Work/one.txt
OK
deny-all.sb
(version 1)
(deny default)
Worked as expected:
$ sandbox-exec -f deny-all.sb ./writefoo /Volumes/Work/two.txt
sandbox-exec: execvp() of './writefoo' failed: Operation not permitted
deny-explicit.sb
(version 1)
(allow default)
(deny file-write* (subpath "/Volumes/Work/forbidden/") )
Worked as expected:
$ sandbox-exec -f deny-explicit.sb ./writefoo /Volumes/Work/five.txt
Write /Volumes/Work/five.txt
OK
$ sandbox-exec -f deny-explicit.sb ./writefoo /Volumes/Work/forbidden/six.txt
Write /Volumes/Work/forbidden/six.txt
Failed file open
allow-explicit.sb
(version 1)
(deny default)
(allow file-write* (subpath "/Volumes/Work/permitted/") )
Did not work as expected:
$ sandbox-exec -f allow-explicit.sb ./writefoo /Volumes/Work/three.txt
sandbox-exec: execvp() of './writefoo' failed: Operation not permitted
$ sandbox-exec -f allow-explicit.sb ./writefoo /Volumes/Work/permitted/four.txt
sandbox-exec: execvp() of './writefoo' failed: Operation not permitted
allow-explicit-2.sb
Trying a variation:
(version 1)
(allow default)
(deny file-write*)
(allow file-write* (subpath "/Volumes/Work/permitted/") )
This did work as expected:
$ sandbox-exec -f allow-explicit-2.sb ./writefoo /Volumes/Work/seven.txt
Write /Volumes/Work/seven.txt
Failed file open
$ sandbox-exec -f allow-explicit-2.sb ./writefoo /Volumes/Work/permitted/eight.txt
Write /Volumes/Work/permitted/eight.txt
OK
allow-explicit-2.sb
Trying a less restrictive variation; maybe read-access is also required, as well as write-access to the containing folder.
(version 1)
(deny default)
(allow file-read*)
(allow file-write*)
Still did not work as expected:
$ sandbox-exec -f allow-explicit-3.sb ./writefoo /Volumes/Work/nine.txt
sandbox-exec: execvp() of './writefoo' failed: Operation not permitted
explicit-all.sb
It looks like (deny default)
is eliminating some addition capability that is required for writing a file. Try
adding every action I could find:
(version 1)
(deny default)
(allow process*)
(allow file*)
(allow network*)
(allow signal)
(allow ipc*)
(allow sysctl*)
(allow system*)
(allow mach*)
(allow iokit*)
(allow user-preference*)
(allow lsopen)
(allow nvram*)
That worked:
$ sandbox-exec -f explicit-all.sb ./writefoo /Volumes/Work/ten.txt
Write /Volumes/Work/ten.txt
OK
explicit-set.sb
Then it was just a matter of pruning the list to obtain the minimum set:
(version 1)
(deny default)
(allow process-exec)
(allow file-read*)
(allow file-write* (subpath "/Volumes/Work/permitted/") )
That worked:
$ sandbox-exec -f explicit-set.sb ./writefoo /Volumes/Work/permitted/eleven.txt
Write /Volumes/Work/permitted/eleven.txt
OK
$ sandbox-exec -f explicit-set.sb ./writefoo /Volumes/Work/twelve.txt
Write /Volumes/Work/twelve.txt
Failed file open
I suspect that the (allow process-exec)
entry is simply required for sandbox-exec
to
be able to launch writefoo
. (That is consistent with the error messages seen earlier.)
The capability (allow file-read*)
was needed to allow for reading of system libraries;
I expect a more restrictive form could be used.
Second Example
Next, I worked on applying a sandbox to the (standard) TextEdit app from the Applications/Utilities folder.
This example worked for me only after hours of experimentation and mining the internet. It worked on macOS 10.13.6 (High Sierra) - that detail is important. Apple is constantly adjusting the security systems from one OS version to the next. It should not be surprising if these examples fail to work quite the same on any other systems.
- Make a copy of the TextEdit app and place it in a folder outside of the Applications folder.
- Rename the copy to TextEditAdHoc.
- Overwrite the entitlements in the copy, by using the following Terminal command:
codesign --force -s - TextEditAdHoc.app/Contents/MacOS/TextEdit
- Determine the location of the temp directory for applications, by using the following Terminal command:
The result will be something like this:
echo $TMPDIR
$ echo $TMPDIR /var/folders/2r/hypbyzds0p9743fxv1__5cm80000gn/T/
- Create a text file,
sandbox.sb
of the following form:Replace the first subpath with the(version 1) (allow default) (deny network*) (deny file-write*) (allow file-write* (subpath "/private/var/folders/2r/hypbyzds0p9743fxv1__5cm80000gn/T/") ) (allow file-write* (subpath "/Users/michael/allowed/") )
$TMPDIR
results. Replace the second subpath,/Users/michael/allowed/
, with the folder into which files are allowed to be saved. - Launch the sandboxed version of TextEdit by using the following Terminal command:
sandbox-exec -f sandbox.sb TextEditAdHoc.app/Contents/MacOS/TextEdit
Usage of temp folder
Note that for TextEdit it is not possible to restrict writing files to an arbitrary folder! When TextEdit saves a file, it first writes to the temporary folder, and then moves the result to the desired location.
Changing the value of $TMPDIR did not work. The reason why is suggested here.
Sandboxing an app with existing entitlements
According to information here,
it is not possible to override the existing entitlements via sandbox-exec
, unless the only two entitlements are
'com.apple.security.app-sandbox
'
and
'com.apple.security.inherit
'.
Removing existing entitlements
Some suggested using an undocumented feature of codesign
:
codesign --remove-signature TextEditAdHoc.app
That worked rarely for me without returning an error message. Often, I got an error like this:
Mac-Pro:Sandbox michael$ codesign --remove-signature TextEditAdHoc.app
error: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/codesign_allocate: can't write output file: /Users/michael/Documents/Projects/Sandbox/TextEditAdHoc.app/Contents/MacOS/TextEdit.cstemp (Invalid argument)
TextEditAdHoc.app: the codesign_allocate helper tool cannot be found or used
I got the best results by overwriting the app with a new ad-hoc signature, as mentioned here and here. This has the desired side effect of removing all the previous entitlements:
codesign --force -s - TextEditAdHoc.app/Contents/MacOS/TextEdit
At this point, I was able to start testing this method with my password manager - to be described in a future post. A key question would be whether or not the password manager uses the write-temp-file-first method that TextEdit does. This method, by the way, is sometimes recommended as a good software-engineering practice, but it would defeat my goal of controlling where sensitive information is being written.
Debugging
- In the past, one or more of the following entries in a sandbox profile file provided useful debugging information. As of macOS 10.13 High Sierra, they no longer appear to work.
(debug allow) (debug deny) (trace "/var/tmp/trace.txt")
- I was able to obtain useful debugging information by booting a partition that had macOS 10.11 El Capitan installed.
Running a sandboxed app then provided output such as the following in the Console app (system.log):
This is a reformatted version of the same information that could be obtained by tailing the appropriate file:
12/9/20 8:39:15.632 AM sandboxd[137]: ([762]) TextEdit(762) deny file-write-data /dev/dtracehelper 12/9/20 8:39:15.653 AM sandboxd[137]: ([762]) TextEdit(762) deny sysctl-read kern.usrstack64 12/9/20 8:39:15.657 AM sandboxd[137]: ([762]) TextEdit(762) deny forbidden-sandbox-reinit
tail -f /var/log/system.log
- The iosnoop script is a systemwide debugging tool that records all file I/O events. I found this useful for figuring out where a third-party program was trying to write files. It is part of the dTrace framework, which is available on macOS. In modern macOS systems, it requires disabling SIP (System Integrity Protection). (See below for details.)
iosnoop
To run iosnoop
:
- Disable SIP:
- Restart and hold down Command-R.
- Select Utilities > Terminal.
- Type
csrutil disable
- Restart.
- Verify that SIP is disabled by typing the Terminal command
csrutil status
- Launch the app to monitored, e.g., TextEdit.
- Find the pid (process ID) of the app.
ps -ax | grep TextEdit
- Start
iosnoop
and specify the pid with the-p
optionFor more information, see thesudo iosnoop -v -p 517
man
page.man iosnoop
- Operate the app and watch the
iosnoop
output. - When done, reenable SIP:
- Restart and hold down Command-R.
- Select Utilities > Terminal.
- Type
csrutil enable
- Restart.
To contact the author, send email.
Share and Enjoy
—
Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware