Sandboxing Apps on Mac


Contents

Introduction
Requirements
Enabling Sandboxing
Adding Entitlements
Misc. notes


Introduction

Apple covers app sandboxing in some detail here and here. Sandboxing is required for apps submitted to the Mac App Store, but it's also recommended for apps that you distribute yourself outside the App Store. But why would you want to do the extra work?

One reason to sandbox is that it means you're ready to go if you decide someday to submit your app to the App Store. For example, all of Microsoft's enormous Office apps for Mac are sandboxed, yet they're not distributed via the App Store. Clearly Microsoft is prepared if they someday choose to go that route.

A less obvious reason is that going through the process of sandboxing can result in a better app, even if you won't be submitting it to the App Store, or even if you don't ultimately distribute the app sandboxed.

Sandboxing forces you to go through an inventory of just what files, resources and services your app requires. You then request only those things and nothing more. Sandboxing's point of the view is that of the user, not you. After all, it's the user's computer, not yours, and the user is allowing your app to execute on their computer and access their files and resources. Limiting your app to only what it needs and accessing those things properly is one way of honoring the user's point of view. (Plus, users are less and less tolerant of apps that are distributed and implemented like in the old days: zipped, unsigned, doing whatever they want.)


Requirements

  1. Cocoa app. If you're using Lazarus, make sure you compile your app with the Cocoa widgetset, not the Carbon widgetset.

  2. Apple Developer ID for codesigning. Only codesigned apps can be sandboxed.

  3. Text editor for creating simple files to do the codesigning. If you're using Xcode, you do everything in Xcode, but if you're using Lazarus or you compile with your own scripts from the command line, you'll have to do the signing yourself manually.

Enabling Sandboxing

To get started, enable sandboxing for your app. This is really quite simple. Create a .plist entitlements file. This is just an XML file that you can create with a text editor. For example, you might create a file named myapp-entitlements.plist that looks this:

<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>com.apple.security.app-sandbox</key>
    <true/>
</dict>
</plist>
To check the syntax of your entitlements file, use the plutil utility:

plutil myapp-entitlements.plist
Now add the entitlements file to the script that you use to codesign your app. The script should look something like this (substitute your name and ID):

codesign --force --deep --verbose --sign "Developer ID Application: Your Name (Your ID)" --entitlements myapp-entitlements.plist "My App.app"
After running the script, check that everything went okay with codesigning by entering this:

spctl --assess --verbose --type execute "My App.app"
Now start your app and see what no longer works.


Adding Entitlements

Entitlement keys are documented here.

File menu

If your app has a File menu with Open, Save and Save As commands, you'll find these don't work by default in a sandboxed app. Add this to your entitlements file and run your codesigning script again:

    <key>com.apple.security.files.user-selected.read-write</key>
    <true/>
Now the commands should work.

Recent files

Normally Cocoa manages the list of recently opened files automatically in an app that has an Open Recent submenu. When the app opens a file, Cocoa adds the file's name to the Open Recent submenu. Later, when you choose this file from the Open Recent submenu, it reopens.

This system does not appear to work with Lazarus apps compiled with the Cocoa widgetset. With Lazarus, everything is done manually, from creating the main menu to opening files. This probably interferes with the normal Cocoa system of opening and managing recent files.

You can create and manage your own Open Recent submenu, but when your sandboxed app tries to open a recent file (from a previous session), it gets blocked because macOS has no way of knowing that the file was selected originally by the user (meaning it's okay to open). It just thinks you're attempting to open an arbitrary file, which is not allowed. Cocoa has a system for keeping track of recently opened files, it's just that Lazarus apps don't seem to be able to utilize it.

Drag and drop

Dragging and dropping a file on your app to open the file still works in a sandboxed app, as does double-clicking the file to open it in your app.

Temporary files

On Mac, Free Pascal's GetTempDir and GetTempFileName functions use the value of the TMPDIR environment variable to determine the path where temporary files should be created. The path will look something like this:

/var/folders/x4/s130kfgx3fngc3w9rg5v8gr80000gn/T/
In a sandboxed app, this path changes automatically to include the bundle ID of your app. For example:

/var/folders/x4/s130kfgx3fngc3w9rg5v8gr80000gn/T/com.mydomain.myapp
As long as you're using those functions consistently throughout your code, you shouldn't have to change anything for a sandboxed app.

Caches, databases, etc. (the sandbox "Container")

The first time you run a sandboxed app, macOS creates a "container" folder for the app in Library/Containers under the user's home folder. It uses the app's bundle ID to name the container folder. This container is for the app's sole use; other apps do not have access to the app's container folder.

You can think of the app's container folder as a kind of "private" home folder. It has its own Library and Documents folders, for example, so if you're already using those locations to store files such as databases that persist across runs of your app, you may not have to make any changes to your code.

If you're using the Foundation framework's NSHomeDirectory function, when your app is sandboxed this function will point to the container folder instead of the user's normal home folder.

Preferences

Normally you use the Foundation framework's NSUserDefaults class to read and write app preferences. Preferences will be stored in the Library/Preferences folder under the user's home folder. NSUserDefaults will read and write a file using your app bundle ID for the file name with a .plist extension. Your code doesn't access this file directly.

In a sandboxed app, preferences are stored in the Library/Preferences folder in your app's container. If you're using NSUserDefaults, you don't have to make any changes.

See the NSMisc.pas unit from here for examples of how to use NSUserDefaults.

Internet access

By default, attempting to access the Internet in a sandboxed app will result in a "Host name resolution failed" error. Enable internet access by adding this entitlement:

    <key>com.apple.security.network.client</key>
    <true/>
Note that OpenSSL-based HTTP clients are not supported in sandboxed apps. This includes Indy, Synapse and FPC's HTTP client. OpenSSL on Mac has been deprecated for a long time, so you should be using one of Apple's APIs instead.

If you need to make GET or POST requests, you can use the ns_url_request.pas unit from here. It uses the Foundation framework's NSURLConnection class.

Printing

The Print dialog still displays by default in a sandboxed app, but the Print button will be disabled. Add this entitlement to enable the Print button:

    <key>com.apple.security.print</key>
    <true/>

Clipboard

The AppKit framework's NSPasteboard class still works in a sandboxed app.

Scripting

As indicated in Apple's technical note, there are restrictions on scripting by sandboxed apps. Here are several workarounds.

  1. If the app you want to script has "access groups," you can add entitlements for them. The example cited in Apple's docs is for the macOS Mail program, which has several access groups. To enable just the ability to compose e-mails via automation, add this:
        <key>com.apple.security.scripting-targets</key>
        <dict>
            <key>com.apple.mail</key>
            <array>
                <string>com.apple.mail.compose</string>
            </array>
        </dict>
    
    You can determine the names of any access groups by looking in the app's .sdef file (normally in the app's Resources folder).

  2. If the app you want to script does not have access groups, you can request a temporary exception for it. For example, Microsoft Excel does not have access groups, so add this to enable scripting access for the entire Excel app:
        <key>com.apple.security.temporary-exception.apple-events</key>
        <string>com.microsoft.Excel</string>
    
  3. Note that word "temporary." To avoid losing the ability to script an app some time in the future, try the suggestions given in this article.
More information about scripting with Pascal is here.


Misc. notes

  1. To revert to a normal non-sandboxed app, just recompile your app. (Entitlements are stored by codesign in the app's executable.) Then delete your app's container from /Library/Containers under the user's home folder.

  2. A quick way to determine if an app is sandboxed is to look in the /Library/Containers folder. If you don't see a container folder for the app (assuming it's been run at least once), then it's not sandboxed. You'll notice that most of Apple's apps are sandboxed, as are Microsoft's.

  3. Although unrelated to sandboxing, if you're distributing your app yourself, be sure to codesign the app's .dmg installer too. Example:

    codesign --force --verbose --sign "Developer ID Application: Your Name (Your ID)" MyApp.dmg
    spctl --assess --verbose --type install MyApp.dmg
    

Copyright 2018 by Phil Hess.

macpgmr (at) icloud (dot) com

First posted Feb. 25, 2018; last edited Feb. 25, 2018.

Code syntax highlighting done with highlight.js.