Series · 4 partsmacOS Security BoundariesPart 1 · You are here
- 1The App Was Sandboxed. The XPC Boundary Still Needed Authorization.You are here
- 2The Permission Was Granted. The Data Use Still Needed a Policy.
- 3The Helper Was Registered. Its Lifetime Outlived the App.
- 4The Event Was Visible. The Detection Still Needed Context.
The macOS boundary in 60 seconds
A macOS application is rarely one process with one set of permissions. The visible app may be sandboxed, notarized, signed, and protected by the Hardened Runtime. Its bundle may also contain an XPC service, login item, launch agent, launch daemon, system extension, command-line tool, or shared container. Each component can run with a different identity, lifetime, sandbox profile, entitlement set, and access to user or system resources.
That architecture is often good security engineering. Privileged or failure-prone work can be moved out of the user interface process and exposed through a narrow interface. But process separation does not create authorization by itself.
The receiving service must still decide:
- which process is connected;
- which code-signing identity that process satisfies;
- whether the caller is allowed to invoke this operation;
- whether the arguments name resources the caller is allowed to control;
- whether the requested action preserves path, ownership, state, and user-consent invariants;
- what authority and persistence remain after the visible app exits.
The core question is therefore not “is the app sandboxed?” It is:
Where does untrusted application intent become a file, process, preference, credential, or system-level effect?
That conversion point is the macOS authority boundary.
Seven protections that answer different questions
macOS layers several security mechanisms. They reinforce one another, but they are not synonyms.
| Mechanism | Primary question it answers | What it does not prove |
|---|---|---|
| Code signing | Which signed code identity does this executable satisfy? | That the code is safe or this caller is authorized for every operation |
| Notarization and Gatekeeper | Did distributed software pass platform distribution and launch checks? | That runtime business logic and local IPC authorization are correct |
| Hardened Runtime | Which process-integrity protections and exceptions apply? | That files, services, tenants, or XPC methods enforce application policy |
| App Sandbox | Which resources may this executable access after compromise? | That another component in the bundle has the same sandbox or authority |
| TCC and user consent | Did the user or administrator permit access to protected data or services? | That the process uses the granted access only for the intended workflow |
| XPC and Service Management | How are separate processes launched and connected? | That the listener authenticates the peer or authorizes each requested effect |
| SIP and platform protections | Which system locations and processes receive operating-system protection? | That third-party application data, helpers, and custom protocols are correct |
Apple describes App Sandbox as a kernel-enforced access-control technology that restricts an app to the capabilities it declares. Its purpose is containment: it reduces the harm available to compromised code. The App Sandbox documentation also makes clear that a sandboxed app can use separately configured tools and services when its design requires them.
The Hardened Runtime protects process integrity against classes of code injection, library loading, and memory-space tampering. Exceptions are expressed as entitlements on executables. It is a different boundary from file, privacy, IPC, and business authorization.
This distinction prevents a common reporting failure: listing platform protections as though their presence closes every path. A strong review records what each control actually constrains, then looks for the authority that crosses between components.
The app bundle is a graph of executables
Apple’s current XPC documentation describes three important service types:
- a bundled XPC service is normally one process per client and follows the client’s lifetime;
- a launch agent runs for the logged-in user and can communicate within that user session;
- a launch daemon is system-wide, may start before login, and runs as root.
Those are different security propositions. Even two binaries signed by the same developer can have different entitlements and access. A review that extracts only the main application’s signature misses the components that may create the most significant effect.
Start with a component and authority inventory
Before exercising an interface, inventory the application bundle and every persistent component associated with it. On a customer-supplied test Mac, read-only inspection can begin with standard platform tools:
APP_PATH="/Applications/Example.app"
codesign -dvvv --entitlements :- "$APP_PATH"
find "$APP_PATH/Contents" -maxdepth 4 -type d \
\( -name "XPCServices" -o -name "LoginItems" -o -name "LaunchAgents" \
-o -name "LaunchDaemons" -o -name "SystemExtensions" \) -print
Repeat signature and entitlement inspection for every Mach-O executable, not just the outer bundle. Record the designated requirement, Team ID, signing identifier, Hardened Runtime state, sandbox entitlement, App Groups, Keychain groups, privacy-sensitive entitlements, runtime exceptions, and restricted framework entitlements.
Then inspect the component’s declared role:
plutil -p "$APP_PATH/Contents/Info.plist"
otool -L "$APP_PATH/Contents/MacOS/Example"
For launch items, preserve the relevant property list, executable location, label, arguments, working directory, environment, user context, and registration state. On macOS 13 and later, Service Management provides the supported model for registering login items, launch agents, and launch daemons from an app bundle. The presence of a helper in Contents/Library describes packaging; the service configuration and runtime identity describe authority.
A useful inventory has one row per executable:
| Field | Question |
|---|---|
| Bundle-relative path | Where is this executable packaged and updated? |
| Code requirement | Which signing identifier and Team ID does it satisfy? |
| Runtime identity | Which UID, session, and launch domain execute it? |
| Sandbox and entitlements | Which platform restrictions and exceptions apply to this executable? |
| Persistence | Is it app-bound, login-bound, on-demand, or system-wide? |
| IPC names and interfaces | Which XPC, Mach, socket, Apple Event, URL, or file interfaces reach it? |
| Accepted callers | How does the receiver determine peer identity and authorization? |
| Privileged operations | Which files, processes, settings, credentials, or devices can it affect? |
| Input ownership | Which paths, identifiers, bookmarks, handles, and state values come from caller? |
| Evidence and uncertainty | What proves each property, and what remains inferred? |
The output is an authority graph. A root process with no useful callable operation may be lower risk than a user process with Full Disk Access and a broad message interface. UID is one dimension, not the risk score.
Entitlements are capabilities, not business authorization
Entitlements tell macOS which protected capability an executable may request or which exception applies. They do not say that every caller of that executable should receive that capability.
Suppose a helper can update a protected configuration file. Its signature and entitlement may be correct. The system may launch it exactly as designed. The helper must still decide:
- Is this connection from an approved client identity?
- Is this client allowed to request this operation now?
- Does the user or administrator authorize the business action?
- Are all referenced paths and objects owned by the expected principal?
- Will the helper open and validate the final resource safely?
- Is the resulting effect bounded, logged, and reversible?
The entitlement answers “may this executable attempt the operation?” The service method answers “may this caller cause this particular effect?”
This is also why TCC and sandbox conclusions must remain separate. User consent can grant a process access to protected data. App Sandbox can restrict filesystem or device access. Neither mechanism automatically validates the semantics of an XPC request or prevents an already-authorized process from misusing a broad operation.
Authenticate the peer, then authorize the operation
XPC gives the client and service a structured communication channel. The service still needs a caller policy. A process name, PID, bundle path, or client-supplied identifier is not a durable code identity.
Modern platform APIs allow an XPC listener or connection to enforce code-signing requirements. Apple documents that xpc_listener_set_peer_requirement checks incoming messages against a peer code-signing requirement and drops requests that do not satisfy it. Foundation’s setCodeSigningRequirement provides the corresponding peer requirement control for NSXPCConnection. You can experiment with these validation primitives and observe policy verdicts in the interactive macOS Code Requirement & Entitlement Evaluator.
Peer identity is the first gate, not the final policy:
A robust decision chain is:
- Peer requirement: accept only the designated client identities and expected signing team.
- Connection context: bind the accepted connection to an immutable server-observed identity.
- Method allowlist: expose narrow operations rather than a generic command, path, or selector.
- Right evaluation: decide whether this caller and user context may request the operation.
- Typed arguments: reject ambiguous types, unexpected keys, excessive sizes, and invalid state.
- Resource binding: resolve caller-controlled references into approved resources without trusting textual paths alone.
- Effect check: confirm the actual file, process, preference, or system state changed exactly as intended.
- Audit and revocation: preserve caller identity, operation, target, outcome, and policy decision; invalidate authority when the app, account, or configuration changes.
The security property belongs to the whole chain. Improving only one gate can leave the same effect reachable through another interface.
The PID check fails in a way a black-box test cannot see
The recommendation above has a measurable consequence, so it is worth measuring rather than asserting.
The lab that accompanies this post now runs two servers that expose the same method and differ in
exactly one respect. One resolves the peer with SecCodeCopyGuestWithAttributes and
kSecGuestAttributePid, taking the PID from connection.processIdentifier. The other calls
setCodeSigningRequirement and lets the platform evaluate the peer against the connection’s audit token.
Under normal conditions the two are indistinguishable from outside. Both accept the approved client and deny the mismatched one. Any assessment that consists of connecting with the wrong identity and checking whether it is refused will score both implementations as sound.
The difference appears only when the caller stops being honest about which binary holds the PID. A client
signed under the denied identity puts its request in flight, then replaces its own process image with a
decoy carrying the approved identity, using posix_spawn with POSIX_SPAWN_SETEXEC. The PID does not
change. The server performs its check afterwards and resolves that PID to the decoy:
scenario client server result
normal allowed weak ALLOW
normal denied weak DENY
normal allowed strict ALLOW
normal denied strict DENY
PID-reuse race denied weak ALLOW <-- bypass
PID-reuse race denied strict DENY
The weak server reports the bypassed request in the same terms it uses for a legitimate one, and then performs the effect for a caller whose own signature it had refused moments earlier:
peer=APPROVED decision=ALLOW gate=code-requirement
effect=BOUNDED input=synthetic
The strict server never reaches that state. setCodeSigningRequirement is evaluated by the platform
against the audit token, which is a kernel-held property of the connection rather than a number the
caller can recycle.
Two conclusions follow, and the second is the one that changes how a review is run. First, a peer check written against a PID is not a weaker version of a code-signing check; it is a different check that happens to agree with the correct one whenever nobody is attacking it. Second, you cannot audit this from the outside without attempting the race or reading the implementation. A service that refuses your unsigned client has told you nothing about whether it would refuse a client that lies.
The bench, the racer, and the decoy are in labs/macos-xpc-authority/. The weak server is kept on
purpose: an instrument that claims to assess peer validation has to be shown catching an implementation
that is genuinely unsound before a clean verdict on a real service means anything.
Path strings are requests, not resources
Privileged helpers often receive paths because the visible application selected a file, created a backup, downloaded an update, or asked for a configuration change. The service must not treat the string as proof that the caller owns the final object.
Between validation and use, filesystem state can change. Relative paths, symbolic links, aliases, mount points, hard links, case behavior, and directory replacement can alter what a textual path resolves to. A useful review asks:
- who created each path component;
- which process owns and can modify the parent directories;
- whether validation and use address the same opened object;
- whether the target may cross out of an approved container or product directory;
- whether the helper follows links or changes ownership and permissions;
- whether temporary files are created safely;
- whether archive extraction, update staging, or restore operations preserve a fixed destination;
- whether a file selected under user consent becomes a generic capability for later operations.
The safest interface carries the narrowest resource representation the operation needs. A helper that exposes “run this command” or “write these bytes to this arbitrary path” has delegated its own authority model to the caller.
Assess the boundary without turning the Mac into a target
A product review should use a dedicated customer-owned test Mac, disposable test accounts, synthetic files, and a reversible operation set. Start with static and observational evidence. Dynamic tests should change one variable at a time.
| Test dimension | Positive control | Negative control |
|---|---|---|
| Peer identity | Approved signed test client connects | Unapproved test client or mismatched requirement is rejected |
| Method | Approved client invokes one documented operation | Same client invokes an unavailable or unauthorized operation |
| User right | Authorized test user performs a permitted administrative task | Ordinary test user receives a denial before the effect |
| Resource ownership | Helper modifies a synthetic product-owned file | Caller-controlled path outside the approved root is rejected |
| Workflow state | Registered component performs a current operation | Removed, disabled, or stale component cannot retain old authority |
| Effect | Exactly one expected change occurs and is logged | Denied request produces no file, process, setting, or permission change |
Stop immediately if a test touches real user data, another account, production credentials, security settings outside the agreed lab, or a system component not owned by the product. Do not disable SIP, bypass TCC prompts, weaken endpoint controls, or install unsigned helpers merely to make a hypothesis easier to demonstrate. If the proof requires changing a platform protection, reframe the test or obtain an explicit lab-specific decision.
A fictional XPC authority review
Assume a product called HarborSync has a sandboxed graphical application and a launch daemon that performs a small set of backup and restore operations. The daemon runs as root and receives requests over XPC. The customer supplies a disposable Mac, two signed test clients, synthetic backup data, and one product-owned destination directory.
- The reviewer inventories the GUI, bundled XPC component, launch daemon, updater, and login item. Each executable receives its own signature, entitlement, UID, persistence, and interface record.
- The GUI is sandboxed and uses security-scoped access for user-selected data. The launch daemon is not treated as inheriting the GUI’s restrictions; its root authority and XPC listener are mapped separately.
- A normal restore from the approved client to the synthetic product directory succeeds. The created file, ownership, mode, audit event, and timestamp establish the positive control.
- A second signed test client from the same development team does not satisfy the listener’s designated requirement. Its request is rejected before the restore method runs. Team membership alone is not the product’s caller policy.
- The approved client requests a restore outside the product-owned root. The daemon rejects the resource after resolving the final destination. No file or temporary artifact appears.
- The customer removes the daemon’s authorization in System Settings. Existing clients lose the operation rather than preserving a hidden long-lived capability.
- One issue is found in the cleanup method: it accepts the correct peer but authorizes deletion using a caller-supplied backup identifier that is not bound to the active user’s backup set. In the synthetic lab, User A can request deletion of User B’s test backup.
- The test stops after one reversible record proves the boundary. The finding is framed as missing object authorization inside an authenticated XPC method—not as a sandbox escape or a macOS platform failure.
This distinction matters. The application was sandboxed. The service accepted the expected signed client. The missing decision was still local business authorization: which backup object this user was permitted to affect.
Evidence matrix
| Observation or result | What it supports | What it cannot prove | Required next step |
|---|---|---|---|
| Main app has the App Sandbox entitlement | The main executable enters App Sandbox | Helper sandbox state, TCC state, XPC policy, or method safety | Inspect every executable and runtime component |
| Bundle is notarized and accepted by Gatekeeper | Distribution and launch checks succeeded | Absence of logic, IPC, update, or authorization vulnerabilities | Map runtime boundaries and privileged effects |
| Hardened Runtime is enabled | Default runtime integrity restrictions apply | Which exception entitlements weaken a component | Inspect executable-specific flags and entitlements |
| XPC service is packaged in the app | A separate process and interface are part of the product | Peer requirements, method authorization, or input ownership | Recover listener policy and operation catalogue |
| Launch daemon runs as root | The component has system-level execution context | Reachability, exploitable operation, or caller authorization | Map every interface and resulting effect |
| Listener enforces a signing requirement | Peers must satisfy one code identity policy | That every accepted peer may invoke every method | Test operation and object authorization |
| Client PID or process name is checked | A transient process attribute influences policy | Stable code identity or resistance to process lifecycle changes | Replace with platform-enforced code requirement where possible |
| Path is canonicalized before use | One textual normalization step occurred | Final-object identity, link safety, or validation/use consistency | Bind the operation to a resolved approved resource |
| Unauthorized request returns an error | One request path reported denial | Absence of side effects, partial writes, or queued work | Verify filesystem, process, configuration, and audit state |
| Synthetic forbidden effect does not occur | The tested negative control preserved the stated invariant | All callers, methods, resources, versions, or lifecycle states | Expand by authority and effect, not blind message mutation |
What developers and defenders should receive
The useful deliverable is not an entitlement dump. It is a component-to-effect model.
Executable inventory. Every app, helper, XPC service, login item, agent, daemon, extension, and tool with code requirement, entitlements, UID, launch domain, persistence, and update relationship.
Authority map. Files, processes, credentials, protected data, system settings, devices, and security events each component can affect.
Interface catalogue. XPC listener names, public methods, argument schemas, error behavior, connection lifetime, peer requirements, and alternate interfaces.
Decision matrix. Accepted client identities, method rights, user context, resource ownership, workflow state, expected effects, and denied controls.
Lifecycle results. Installation, registration, upgrade, disablement, logout, user switch, uninstall, and stale-client behavior. Persistent authority should have a visible owner and a defined revocation path.
Remediation contract. The exact receiver-side decision to add or narrow, the component owner, the minimum code requirement, the allowed methods and resources, effect-level logging, and a retest that proves both the intended success and intended denial.
The macOS topic begins with authority, not checklists
macOS Security will follow a consistent route:
- App Sandbox and XPC authority: where one process asks another to create a stronger effect.
- TCC and consent boundaries: how user-approved access becomes persistent application capability.
- Launch services and persistence: how login items, agents, daemons, updates, and ownership survive across lifecycle changes.
- Endpoint Security and observability: what defenders can prove about process, file, mount, and authorization events without confusing telemetry with prevention.
The platform already supplies strong primitives: signing requirements, entitlements, sandboxes, Hardened Runtime, user consent, Service Management, and structured IPC. The security review asks whether the product composed those primitives into the intended authority model.
The app window is only the visible component. The boundary is the moment a specific caller, method, resource, and state become a real system effect.
MITRE ATT&CK mapping
| Tactic | Technique ID | Technique name | Verification signal |
|---|---|---|---|
| Execution | T1559.003 | Inter-Process Communication: XPC Services | launchctl print gui/$UID/<service> connection inspect |
| Privilege Escalation | T1548 | Abuse Elevation Control Mechanism | Audit caller code requirement against receiving helper |
| Defense Evasion | T1553.001 | Subvert Trust Controls: Code Signing | Check peer codesign -dvvv and team ID boundary |
Minimal reproducible Swift client, privileged helper server, and signature requirement verification primitives are available in the project repository under labs/macos-xpc-authority/.
How current is this note?
The latest source-review, content-update, or publication date is shown.
Primary public records were checked. Environment-specific behavior remains outside the claim unless separately reproduced.
- developer.apple.com · protecting-u…th-app-sandbox ↗
- developer.apple.com · hardened-runtime ↗
- developer.apple.com · xpc ↗
- developer.apple.com · xpc_listener…er_requirement ↗
- developer.apple.com · setcodesigni…equirement(_:) ↗
- developer.apple.com · servicemanagement ↗
- support.apple.com · web ↗
- theevilbit.github.io · secure_coding_xpc_part5 ↗
- knight.sc · audit-tokens-explained ↗
