Hardened Runtime and Sandboxing Revisited

November 7 2019 by Jeff Johnson

This is a follow-up to my earlier blog post as well as a response to a a blog post by the developers of GitFinder that was highlighted by Michael Tsai. Although GitFinder is distributed outside the Mac App Store, the developers nonetheless chose to sandbox it. The GitFinder app does not have the sandbox entitlement to access a user's Contacts (com.apple.security.personal-information.addressbook), but the GitFinder app does embed an xpc service with that sandbox entitlement. The xpc service accesses some information in the user's Contacts and passes that information back to the app. GitFinder does not currently enable the hardened runtime, but next year Apple will require all apps to enable the hardened runtime in order to get notarized for distribution outside the Mac App Store. The developers of GitFinder are upset because enabling the hardened runtime requires giving the Contacts sandbox entitlement to the app as well as to the embedded xpc service. Why is that necessary?

The sandbox and the hardened runtime are independent but overlapping technologies, as I explained in my earlier blog post. Since they are independent, each operates according to its own separate rules. The sandbox allows the xpc service and the main executable to have different sandbox entitlements, which is how one can have the Contacts entitlement and the other not. In contrast, the hardened runtime depends only on the main executable of the app. For simplicity, let's set aside the sandbox for a moment and consider a non-sandboxed app with a non-sandboxed embedded xpc service. If the main executable has the hardened runtime enabled, and it has the Contacts hardened runtime entitlement, then both the main executable and the xpc service can access the Contacts API, regardless of whether the xpc service has the Contacts entitlement. The hardened runtime entitlements of the embedded xpc are irrelevant, only the main executable matters. Likewise, if the main executable does not have the Contacts hardened runtime entitlement, then the xpc service will not be able to access the Contacts API, even if the xpc service has the Contacts entitlement. Moreover, user permission to access Contacts is granted to the app as a whole, including both the main executable and the xpc service, not to each separately. If the app has the Contacts hardened runtime entitlement, the xpc service accesses the Contacts API, and the user clicks "OK" to grant access to Contacts, then the main executable will also be able to access the Contacts API without causing another user consent dialog to display. The reverse is also true: if the main executable accesses the Contacts API first, and the user grants permission, then the xpc service is automatically included in that permission.

The hardened runtime and the sandbox are both restrictive technologies. If the hardened sandbox grants an entitlement, but the sandbox doesn't grant the entitlement too, then the entitlement is not effective. The app is still blocked unless the hardened runtime and the sandbox are in agreement. Here's the catch: for better or worse, the hardened runtime and the sandbox use the same name for some entitlements. For example, they both use com.apple.security.personal-information.addressbook for the Contacts entitlement. As a consequence, if an app is both hardened and sandboxed, it's impossible to give the Contacts hardened runtime entitlement to the main executable without also giving the Contacts sandbox entitlement to the main executable! That's the dilemma troubling the developers of GitFinder.

Is this situation really a problem? In my opinion, I'm not sure that it is a problem. I don't think the architecture of GitFinder reflects the intent behind Apple's XPC API. According to Apple's documentation (which is unfortunately "archived"), "There are two main reasons to use XPC services: privilege separation and stability." Stability means that an xpc service can crash without causing causing the entire app to crash. Privilege separation means resistance to attack:

Modern applications increasingly rely on untrusted data, such as web pages, files sent by email, and so on. This represents a growing attack vector for viruses and other malware.

With traditional applications, if an application becomes compromised through a buffer overflow or other security vulnerability, the attacker gains the ability to do anything that the user can do. To mitigate this risk, Mac OS X provides sandboxing—limiting what types of operations a process can perform.

In a sandboxed environment, you can further increase security with privilege separation—dividing an application into smaller pieces that are responsible for a part of the application’s behavior. This allows each piece to have a more restrictive sandbox than the application as a whole would require.

The purpose of privilege separation in this context is to give the xpc service fewer privileges than the app. "By default, XPC services are run in the most restricted environment possible—sandboxed with minimal filesystem access, network access, and so on. Elevating a service’s privileges to root is not supported. Further, an XPC service is private, and is available only to the main application that contains it."

That's what the Apple documentation says. On the other hand, here's what the GitFinder developers say: "If an application needs access to contacts, it should not access them directly, but from a dedicated XPC service instead, passing required data back to the application. The service is given only AddressBook (Contacts) sandbox entitlement, so in case it ever gets compromised, no damage to user’s personal data could be done, as the service cannot access disks nor network." This reasoning seems strange to me. Software doesn't simply get compromised. What's the attack vector? If you have network attackers, for instance, then of course they can't compromise an xpc that doesn't have network access. But that's not very interesting. If the xpc passes Contacts info back the app, and network attackers compromise the app, then the network attackers have the Contacts info! The attackers may not have unlimited access to Contacts, but they still have some illicit access to Contacts. So you really haven't solved the problem there. This architecture seems to be the opposite of what you'd want. For true protection, you want the network access in the xpc service, and the Contacts entitlement in the main app. (This is the architecture of Apple's own apps such as Safari.) If network attackers compromise the xpc service, then the attackers can't access Contacts at all, because the xpc service would be very strictly sandboxed. That's a full solution to protecting Contacts.

One of the purposes of an xpc service is to do "dangerous" operations inside the service. Network access is dangerous, which is why you may want to strictly sandbox it. The data that comes over the network may be maliciously crafted by an attacker. Contacts API access is not dangerous in itself, because the Contacts info already on disk is presumably not maliciously crafted. There's not much compromise risk inherent to making those API calls. The GitFinder developers worry that the "completely unnecessary sandbox entitlement defined for the application only increases chances of damages, as the application, being bigger and more complicated, is much easier target for security attacks than tiny dedicated XPC service is." Again, I think this is kind of backwards. The goal of the xpc service is not to limit the damage the app can do if it's compromised, the goal is to prevent the app from getting compromised in the first place. "What if the main executable gets compromised?" is the wrong question in the context of embedded xpc services. This is clear if we think in terms of stability rather than privilege separation. "What if the main executable crashes?" is not an interesting question, because if the main executable crashes, it's game over for the xpc services too. The goal is to prevent the main executable from crashing by putting dangerous, crash-prone operations inside the xpc service. Then the service can crash without bringing down the app. The damage is limited by disempowering the xpc service, not by empowering the xpc service. Likewise, the key to protecting apps from compromise is not to move entitlements from the main executable to xpc services. They key is to keep the powerful entitlements in the main executable and move compromise-prone operations to xpc services.

A final note of clarification: it's important not to conflate an app's embedded xpc services with launch daemons that run as root. Those are entirely different beasts, even though they (perhaps unfortunately) share some of the same API. The architectural considerations for launch daemons are very different. They aren't embedded in an app, they aren't owned by an app. Thus, the issues in this blog post don't apply directly to them.

Jeff Johnson (My apps, PayPal.Me)