blog

2020-12-17

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:

Just to be clear, the sandbox profile format is not documented for third party use. Feel free to experiment with this stuff, but please don’t try to ship a product based on it.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

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.

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.

  1. Make a copy of the TextEdit app and place it in a folder outside of the Applications folder.
  2. Rename the copy to TextEditAdHoc.
  3. Overwrite the entitlements in the copy, by using the following Terminal command:
    codesign --force -s - TextEditAdHoc.app/Contents/MacOS/TextEdit
    
  4. Determine the location of the temp directory for applications, by using the following Terminal command:
    echo $TMPDIR
    
    The result will be something like this:
    $ echo $TMPDIR
    /var/folders/2r/hypbyzds0p9743fxv1__5cm80000gn/T/
    
  5. Create a text file, sandbox.sb of the following form:
    (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/") )
    
    Replace the first subpath with the $TMPDIR results. Replace the second subpath, /Users/michael/allowed/, with the folder into which files are allowed to be saved.
  6. 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
Sometimes it appeared to complete without error, but the app didn't work afterwards.

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

iosnoop

To run iosnoop:

  1. Disable SIP:
    • Restart and hold down Command-R.
    • Select Utilities > Terminal.
    • Type csrutil disable
    • Restart.
  2. Verify that SIP is disabled by typing the Terminal command
    csrutil status
    
  3. Launch the app to monitored, e.g., TextEdit.
  4. Find the pid (process ID) of the app.
    ps -ax | grep TextEdit
    
  5. Start iosnoop and specify the pid with the -p option
    sudo iosnoop -v -p 517
    
    For more information, see the man page.
    man iosnoop
    
  6. Operate the app and watch the iosnoop output.
  7. When done, reenable SIP:
    • Restart and hold down Command-R.
    • Select Utilities > Terminal.
    • Type csrutil enable
    • Restart.


To contact the author, send email.