← Back to DevBytes

macOS App Sandboxing

Introduction to macOS App Sandboxing

macOS App Sandboxing is a security mechanism introduced by Apple that restricts an application's access to system resources, files, network endpoints, and hardware devices. Inspired by the sandbox model used in iOS, sandboxing on macOS isolates apps so that even if an app is compromised by malicious code, the damage it can inflict is limited to the resources explicitly granted to it. For developers building macOS applications, understanding and correctly implementing sandboxing is not just a best practice — it is a requirement for distribution through the Mac App Store.

What Is App Sandboxing?

At its core, App Sandboxing is a kernel-level enforcement mechanism built on top of macOS's TrustedBSD framework. When an app is sandboxed, the operating system wraps it in a container that defines strict boundaries around what the app can read, write, execute, and communicate with. These boundaries are declared through entitlements — key-value pairs in a property list file that the system reads at launch time.

Without sandboxing, a macOS app runs with the full privileges of the user who launched it. That means a compromised app could read sensitive documents, access the camera, exfiltrate data over the network, or modify system files the user has permission to touch. Sandboxing flips this model: by default, the app has access to almost nothing, and the developer must explicitly request each capability the app needs.

Why Sandboxing Matters

There are several reasons why sandboxing is critical for modern macOS development:

How App Sandboxing Works

Sandboxing is enforced through a combination of code signing, entitlements, and the macOS kernel. When you build a sandboxed app, Xcode signs the app bundle and embeds an entitlements file. At launch, the kernel reads these entitlements and applies a sandbox profile that restricts the process accordingly.

The key components involved are:

The App Container

When a sandboxed app launches for the first time, macOS creates a container directory specifically for that app. This directory serves as the app's home folder and is located at ~/Library/Containers/<bundle-identifier>. Inside this container, the app has full read-write access. Outside of it, access is restricted unless explicitly granted through entitlements or user-selected files.

You can retrieve the path to your container programmatically using Foundation APIs:

import Foundation

let fileManager = FileManager.default

// Get the app's container URL
if let containerURL = fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first {
    print("Container URL: \(containerURL.path)")
    
    // Create a subdirectory for app data
    let dataDirectory = containerURL.appendingPathComponent("MyAppData")
    try? fileManager.createDirectory(at: dataDirectory,
                                     withIntermediateDirectories: true,
                                     attributes: nil)
}

Inside the container, the standard directory structure mirrors the user's home folder. You will find Data, Library, and other familiar subdirectories. The app should treat this container as its primary storage location for user data, preferences, caches, and support files.

Enabling App Sandboxing in Xcode

The easiest way to enable sandboxing is through Xcode's Signing & Capabilities tab. When you enable the App Sandbox capability, Xcode automatically generates an entitlements file and adds the necessary keys.

Step-by-Step Setup

After enabling the capability, Xcode creates a file named <YourApp>.entitlements. Here is what a typical entitlements file looks like:

<?xml version="1.0" encoding="UTF-8"?>
<!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/>
    <key>com.apple.security.files.user-selected.read-write</key>
    <true/>
    <key>com.apple.security.network.client</key>
    <true/>
    <key>com.apple.security.device.camera</key>
    <true/>
</dict>
</plist>

Each key in this plist corresponds to a specific entitlement. The com.apple.security.app-sandbox key is the master switch that enables sandboxing. The remaining keys grant specific capabilities.

Common Entitlements and Their Uses

Apple provides a wide range of entitlements that you can request. Choosing the right set is essential — you should request only what your app genuinely needs, following the principle of least privilege.

File Access Entitlements

By default, a sandboxed app can only access files within its own container. To access files elsewhere, you need specific entitlements or user interaction.

// Entitlement: User-selected read-write file access
// com.apple.security.files.user-selected.read-write

// This allows the app to read and write files the user
// explicitly selects through NSOpenPanel or NSSavePanel.

import Cocoa

let openPanel = NSOpenPanel()
openPanel.allowedContentTypes = [.pdf, .plainText]
openPanel.allowsMultipleSelection = false
openPanel.canChooseDirectories = false

openPanel.begin { response in
    if response == .OK, let url = openPanel.url {
        // The system grants access to this URL via a security-scoped bookmark
        do {
            let data = try url.bookmarkData(options: .withSecurityScope,
                                            includingResourceValuesForKeys: nil,
                                            relativeTo: nil)
            // Store the bookmark data for future access
            UserDefaults.standard.set(data, forKey: "selectedFileBookmark")
            
            // Start accessing the security-scoped resource
            let didStartAccessing = url.startAccessingSecurityScopedResource()
            defer {
                if didStartAccessing {
                    url.stopAccessingSecurityScopedResource()
                }
            }
            
            // Now you can read the file
            let content = try String(contentsOf: url, encoding: .utf8)
            print("File content: \(content)")
        } catch {
            print("Error accessing file: \(error)")
        }
    }
}

For apps that need broader file system access, there are additional entitlements:

Network Entitlements

If your app needs to communicate over the network, you must declare the appropriate entitlement. There are two primary network entitlements:

// Example: Making a network request in a sandboxed app
// Requires: com.apple.security.network.client entitlement

import Foundation

func fetchData(from urlString: String) {
    guard let url = URL(string: urlString) else { return }
    
    let task = URLSession.shared.dataTask(with: url) { data, response, error in
        if let error = error {
            print("Network error: \(error.localizedDescription)")
            return
        }
        
        guard let httpResponse = response as? HTTPURLResponse,
              httpResponse.statusCode == 200,
              let data = data else {
            print("Invalid response")
            return
        }
        
        print("Received \(data.count) bytes")
    }
    
    task.resume()
}

fetchData(from: "https://api.example.com/data")

Hardware Access Entitlements

Access to hardware devices like the camera, microphone, and USB devices requires specific entitlements. These also typically trigger user permission prompts at runtime.

// Example: Accessing the camera in a sandboxed app
// Requires: com.apple.security.device.camera entitlement

import AVFoundation

func requestCameraAccess() {
    let status = AVCaptureDevice.authorizationStatus(for: .video)
    
    switch status {
    case .authorized:
        startCameraSession()
    case .notDetermined:
        AVCaptureDevice.requestAccess(for: .video) { granted in
            DispatchQueue.main.async {
                if granted {
                    startCameraSession()
                } else {
                    print("Camera access denied by user")
                }
            }
        }
    case .denied, .restricted:
        print("Camera access denied. Please enable in System Settings.")
    @unknown default:
        break
    }
}

func startCameraSession() {
    guard let device = AVCaptureDevice.default(for: .video) else {
        print("No camera available")
        return
    }
    print("Camera ready: \(device.localizedName)")
}

Inter-Process Communication

Sandboxed apps cannot freely communicate with other processes. If your app needs to exchange data with another app or a helper tool, you must use approved IPC mechanisms and declare the appropriate entitlements.

// Example: Using XPC services for IPC in a sandboxed app
// XPC services are the recommended way to perform privileged
// operations or communicate between processes.

import Foundation

// Define a protocol that both sides agree on
@objc protocol HelperProtocol {
    func performCalculation(_ values: [Double], 
                            withReply reply: @escaping (Double) -> Void)
}

class XPCClient {
    private var connection: NSXPCConnection?
    
    func connect() {
        connection = NSXPCConnection(serviceName: "com.example.app.Helper")
        connection?.remoteObjectInterface = NSXPCInterface(interface: HelperProtocol.self)
        connection?.resume()
    }
    
    func calculateAverage(_ values: [Double], completion: @escaping (Double) -> Void) {
        guard let proxy = connection?.remoteObjectProxy as? HelperProtocol else {
            completion(0)
            return
        }
        
        proxy.performCalculation(values) { result in
            DispatchQueue.main.async {
                completion(result)
            }
        }
    }
}

Security-Scoped Bookmarks

One of the most important concepts in sandboxed file access is the security-scoped bookmark. When a user selects a file through an NSOpenPanel, the app receives temporary access to that file. However, this access does not persist across app launches. To maintain access to a file across launches, you must create a security-scoped bookmark and store it.

import Foundation

class BookmarkManager {
    static let shared = BookmarkManager()
    private let defaults = UserDefaults.standard
    private let bookmarkKey = "fileBookmarks"
    
    // Save a bookmark for a user-selected URL
    func saveBookmark(for url: URL, withKey key: String) throws {
        let bookmarkData = try url.bookmarkData(
            options: .withSecurityScope,
            includingResourceValuesForKeys: nil,
            relativeTo: nil
        )
        defaults.set(bookmarkData, forKey: key)
    }
    
    // Resolve a stored bookmark back to a URL
    func resolveBookmark(withKey key: String) -> URL? {
        guard let bookmarkData = defaults.data(forKey: key) else {
            return nil
        }
        
        var isStale = false
        do {
            let url = try URL(
                resolvingBookmarkData: bookmarkData,
                options: .withSecurityScope,
                relativeTo: nil,
                bookmarkDataIsStale: &isStale
            )
            
            if isStale {
                // The bookmark is stale; create a new one
                try saveBookmark(for: url, withKey: key)
            }
            
            return url
        } catch {
            print("Failed to resolve bookmark: \(error)")
            return nil
        }
    }
    
    // Convenience method to access a bookmarked file
    func withBookmarkedFile(key: String, _ block: (URL) throws -> Void) rethrows {
        guard let url = resolveBookmark(withKey: key) else { return }
        
        let didStartAccessing = url.startAccessingSecurityScopedResource()
        defer {
            if didStartAccessing {
                url.stopAccessingSecurityScopedResource()
            }
        }
        
        try block(url)
    }
}

// Usage example
let manager = BookmarkManager.shared

// After user selects a file via NSOpenPanel:
// try manager.saveBookmark(for: selectedURL, withKey: "projectFile")

// Later, to access the file:
manager.withBookmarkedFile(key: "projectFile") { url in
    let content = try String(contentsOf: url, encoding: .utf8)
    print("Loaded: \(content)")
}

The critical pattern here is the pairing of startAccessingSecurityScopedResource() and stopAccessingSecurityScopedResource(). Every time you access a bookmarked file, you must start the security scope before reading or writing, and stop it when you are done. Failing to stop the scope leaks kernel resources.

Temporary Exceptions and Migration

For apps transitioning to sandboxing, Apple provides temporary exception entitlements that allow access to resources not covered by standard entitlements. These are intended as a bridge during migration and are reviewed carefully by Apple during App Store review.

<!-- Temporary exception entitlements example -->
<key>com.apple.security.temporary-exception.files.absolute-path.read-write</key>
<array>
    <string>/usr/local/shared/</string>
</array>
<key>com.apple.security.temporary-exception.mach-lookup.global-name</key>
<array>
    <string>com.example.legacy-service</string>
</array>

Temporary exceptions should be avoided in new apps. They are a signal that your app's architecture needs adjustment to work within the sandbox model. If you find yourself needing temporary exceptions, consider refactoring your code to use XPC services, user-selected file access, or other sandbox-friendly approaches.

Debugging Sandbox Issues

When a sandboxed app attempts to access a resource it is not entitled to, the operation silently fails or returns an error. The system logs the violation, but you need to know where to look.

Using the Console App

The macOS Console app is your primary tool for diagnosing sandbox violations. Open Console, select your Mac in the sidebar, and filter by your app's process name or by the sandboxd process. Violations appear as messages like:

Sandbox: MyApp(12345) deny(1) file-write-data /Users/username/Documents/secret.txt

This message tells you that your app attempted to write to a file outside its sandbox and was denied. The solution is either to request the appropriate entitlement or to use an NSOpenPanel to get user-selected access.

Programmatic Error Handling

Always handle file operation errors gracefully in sandboxed apps. A write that succeeds during development might fail in a sandboxed context if the entitlements are misconfigured.

import Foundation

func writeDataSafely(_ data: Data, to filename: String) {
    let fileManager = FileManager.default
    
    // Always write within the container
    guard let containerURL = fileManager.urls(
        for: .applicationSupportDirectory,
        in: .userDomainMask
    ).first else {
        print("Could not locate container directory")
        return
    }
    
    let fileURL = containerURL.appendingPathComponent(filename)
    
    do {
        try data.write(to: fileURL, options: .atomic)
        print("Successfully wrote data to: \(fileURL.path)")
    } catch let error as NSError {
        if error.domain == NSCocoaErrorDomain {
            switch error.code {
            case NSFileWriteNoPermissionError:
                print("Permission denied — check sandbox entitlements")
            case NSFileWriteOutOfSpaceError:
                print("Disk is full")
            default:
                print("Write error: \(error.localizedDescription)")
            }
        } else {
            print("Unexpected error: \(error.localizedDescription)")
        }
    }
}

Best Practices for Sandboxed Apps

Request Minimal Entitlements

Follow the principle of least privilege. Every entitlement you add expands the attack surface of your app. Review your entitlements file regularly and remove any that are no longer needed. Apple reviewers also scrutinize entitlement requests, so unnecessary entitlements can delay approval.

Store Data in the Container

Always store app-generated data within the container directory. Use the standard directory enums (applicationSupportDirectory, cachesDirectory, documentDirectory) so that the system automatically routes paths to the correct container location. Never hardcode paths like ~/Documents or /tmp, as these will fail in a sandboxed environment.

Use XPC for Privileged Operations

If your app needs to perform operations that require elevated privileges or access to resources outside the sandbox, split those operations into a separate XPC service. XPC services run in their own sandbox with their own entitlements, allowing you to isolate privileged code from your main app.

// Creating an XPC service target in Xcode:
// 1. File > New > Target
// 2. Choose "XPC Service"
// 3. Name it (e.g., "PrivilegedHelper")
// 4. Implement the protocol in the main protocol file
// 5. Configure the helper's entitlements separately

// Helper service implementation (in the XPC target):
import Foundation

class PrivilegedHelper: NSObject, HelperProtocol {
    func performCalculation(_ values: [Double],
                            withReply reply: @escaping (Double) -> Void) {
        let sum = values.reduce(0, +)
        let average = values.isEmpty ? 0 : sum / Double(values.count)
        reply(average)
    }
}

// In the XPC service's main.swift or PrincipalClass:
class ServiceDelegate: NSObject, NSXPCListenerDelegate {
    func listener(_ listener: NSXPCListener,
                  shouldAcceptNewConnection newConnection: NSXPCConnection) -> Bool {
        newConnection.exportedInterface = NSXPCInterface(interface: HelperProtocol.self)
        newConnection.exportedObject = PrivilegedHelper()
        newConnection.resume()
        return true
    }
}

let delegate = ServiceDelegate()
let listener = NSXPCListener.service()
listener.delegate = delegate
listener.resume()

Handle Permission Denials Gracefully

Users can revoke permissions at any time through System Settings. Your app should handle denied permissions without crashing. Always check authorization status before attempting to access protected resources, and provide clear guidance to the user when permissions are missing.

import AVFoundation
import AppKit

func checkAndRequestMicrophoneAccess() {
    let status = AVCaptureDevice.authorizationStatus(for: .audio)
    
    switch status {
    case .authorized:
        startRecording()
    case .notDetermined:
        AVCaptureDevice.requestAccess(for: .audio) { granted in
            DispatchQueue.main.async {
                granted ? startRecording() : showPermissionDeniedAlert()
            }
        }
    case .denied:
        showSettingsRedirectAlert()
    case .restricted:
        showRestrictedAlert()
    @unknown default:
        break
    }
}

func showSettingsRedirectAlert() {
    let alert = NSAlert()
    alert.messageText = "Microphone Access Required"
    alert.informativeText = "Please enable microphone access in System Settings > Privacy & Security."
    alert.addButton(withTitle: "Open Settings")
    alert.addButton(withTitle: "Cancel")
    
    if alert.runModal() == .alertFirstButtonReturn {
        if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Microphone") {
            NSWorkspace.shared.open(url)
        }
    }
}

func startRecording() {
    print("Recording started")
}

func showPermissionDeniedAlert() {
    print("Microphone permission denied")
}

func showRestrictedAlert() {
    print("Microphone access restricted by device policy")
}

Test with Sandbox Enabled Early

Enable sandboxing early in the development process, not as a last step before submission. Many issues — file access failures, network errors, IPC problems — are much easier to fix during development than right before release. Create a testing checklist that verifies each entitlement works as expected in a clean sandbox environment.

Use App Groups for Shared Data

If your app suite includes multiple apps or app extensions that need to share data, use App Groups. App Groups provide a shared container that all group members can access, even in a sandboxed environment.

// Enable App Groups in Signing & Capabilities
// Add a group identifier like: group.com.example.shared

import Foundation

func sharedContainerURL() -> URL? {
    return FileManager.default.containerURL(
        forSecurityApplicationGroupIdentifier: "group.com.example.shared"
    )
}

func writeToSharedContainer(data: Data, filename: String) throws {
    guard let containerURL = sharedContainerURL() else {
        throw NSError(domain: "AppGroup", code: 1,
                      userInfo: [NSLocalizedDescriptionKey: "Shared container not available"])
    }
    
    let fileURL = containerURL.appendingPathComponent(filename)
    try data.write(to: fileURL, options: .atomic)
    print("Wrote to shared container: \(fileURL.path)")
}

Common Pitfalls and Solutions

Hardcoded File Paths

One of the most common mistakes when sandboxing an existing app is hardcoded file paths. Code that writes to ~/Documents/myfile.txt will fail silently in a sandbox. Replace all hardcoded paths with container-relative paths obtained through FileManager URL enums.

// BAD: Hardcoded path — will fail in sandbox
let badURL = URL(fileURLWithPath: "~/Documents/data.json")

// GOOD: Container-relative path — works in sandbox
let fileManager = FileManager.default
let goodURL = fileManager.urls(for: .documentDirectory, in: .userDomainMask)
    .first!
    .appendingPathComponent("data.json")

Shell Command Execution

Sandboxed apps cannot freely execute shell commands via Process (formerly NSTask). If your app relies on calling command-line tools, you will need to find native API alternatives or use an XPC helper with the appropriate entitlements. Many common shell operations have direct Foundation equivalents:

Forgetting to Stop Security Scopes

Every call to startAccessingSecurityScopedResource() must be paired with a corresponding stopAccessingSecurityScopedResource(). Failing to do so leaks file descriptors and can eventually cause your app to run out of resources. Use Swift's defer statement to guarantee cleanup:

func readFile(at url: URL) throws -> String {
    let didStart = url.startAccessingSecurityScopedResource()
    defer {
        if didStart {
            url.stopAccessingSecurityScopedResource()
        }
    }
    return try String(contentsOf: url, encoding: .utf8)
}

Conclusion

macOS App Sandboxing is a fundamental security architecture that every macOS developer must understand and embrace. By isolating apps within strict boundaries and requiring explicit permission for each capability, sandboxing protects users from malicious exploitation and gives them confidence in the software they install. While adopting sandboxing can require significant refactoring of existing code — especially around file access, inter-process communication, and shell command usage — the resulting apps are more secure, more stable, and eligible for Mac App Store distribution. By following the principle of least privilege, using security-scoped bookmarks for persistent file access, leveraging XPC services for privileged operations, and testing with sandboxing enabled from the start of development, you can build robust macOS applications that respect user privacy while delivering powerful functionality within the sandbox's protective walls.

— Ad —

Google AdSense will appear here after approval

← Back to all articles