Android IPC Security Considerations

Generally Chrome communicates between its processes using the Mojo inter-process communication (IPC) mechanism. For most features, this is the preferred IPC mechanism to use. However, as an Android application, there are certain interactions with other applications and the Android OS that necessitate using different IPC mechanisms to communicate. This document covers security concerns related to those Android-specific IPC mechanisms.

The Chrome browser process is typically the only process type that will interact with these different IPC mechanisms.

Intents

Intents are the most common type of inter-process communication mechanism on Android. They are most commonly used to start Activities and they internally carry data associated with that Activity (e.g. using the ACTION_SEND Intent to share a piece of content and including either text or image data in the Intent body).

Inbound Intents

Because any application can dispatch Intents with Chrome as the receiver, when receiving an inbound Intent, you should never fully trust the data contained within. Data sent from other applications could be malicious or malformed, and so you must validate or sanitze the data before passing it to other trusted components of the browser process. Intents are handled in Java though, so following the Rule of 2 is generally easy. (Though take note that certain Android classes are just Java wrappers around native code, which would not be considered safe by that rule.)

It is fundamentally impossible to determine the sender of an Intent, so the security model of your feature cannot depend on authenticating the sender. Do not trust Intent.EXTRA_REFERRER. See also the discussion below about capability tokens.

One way to authorize Intents is to use the system‘s android:permission attribute on a component’s (e.g. Activity, Service, etc.) manifest declaration. You can define a custom permission and set the android:protectionLevel of the permission to "signature" or "signatureOrSystem" to restrict access to just components signed by the same certificate (or trusted system components).

Outbound Intents

There are two types of Intents: implicit and explicit. With implicit Intents, the receiving application is not specified by the sender and the system uses a resolution process to find the most suitable component to handle it. An implicit Intent can sometimes result in a chooser being shown to the user when multiple applications could handle it. Explicit Intents specify either the package name or a fully qualified ComponentName, so the recipient is known at the time it is dispatched. Implicit Intents can result in an unexpected (and maybe malicious) application receiving user data. If it is possible to know the target application when sending an Intent, always prefer using an explicit Intent.

PendingIntents

A PendingIntent is created by one application and vended to another. The object allows the receiving application to start the component (i.e. Activity, Service, Broadcast) as if the creating application started it. Similar to a setuid binary, you must use this with care, as it can even be used to start non-exported components of the creating application.

It is possible to retrieve information about the creator package of the PendingIntent using the getCreatorPackage() method. This is the identity under which the Intent, which the PendingIntent represents, will be started. Note that you cannot retrieve specific information about the Intent (e.g. its target and extras). And as discussed above with Intents, it is not possible to determine the application that called PendingIntent.send().

Binder

Binder is the low level IPC mechanism on Android, and it is what Intents and other Framework-level primitives are built upon.

Bound Services

To communicate between components using Binder, you declare a <service> in your manifest and connect to it using Context.bindService(). This is referred to a as a bound service.

One of the powerful properties of a bound service is that you can determine the identity of your communicating peer. This can only be done during a Binder transaction (e.g. in an AIDL method implementation or a Handler.Callback) that is not marked FLAG_ONEWAY. During the transaction use Binder.getCallingUid() to retrieve the package's UID.

In Android, every installed application is given a unique user ID (UID). This can be used as a key to query the PackageManager, to retrieve the PackageInfo for the application. With the PackageInfo, information about the applications code signing certificates can be retrieved and cryptographically authenticated. This is a strong authentication check and it is the only reliable mechanism by which you can authenticate your peer.

In Chrome, the helper functions ExternalAuthUtils.isCallerValid() and isCallerValidForPackage() can perform these checks for you.

Capability Tokens

We define a capability token to be an unforgeable object that the holder may present to another application as authentication to access a specific capability. Binder objects are backed by the kernel (i.e. are unforgeable), are transferable, and are comparable using isEqual(), so Binders can be used as capability tokens.

One security factor to bear in mind is that because capability tokens are transferable, they do not strongly authenticate a caller's identity. One application may deliberately or accidentally transfer a capability token to another application, or a token could be exfiltrated via an application logic vulnerability. Therefore, only use capability tokens for access control, not identity authentication.

While noting the above factor, capability tokens can be useful for authenticating Intents. If two applications have established a Binder connection, they can use the channel to exchange a capability token. One application constructs a generic Binder (using the Binder(String) constructor) and sends the object over that ServiceConnection to the other application, while retaining a reference to it.

The generic Binder object can then be transmitted as an Intent extra when sending Intents between the two applications. By comparing the object with Binder.isEqual(), you can validate the capability token. Be sure to use an explicit Intent when sending such an Intent.

This same approach can also be done with using a PendingIntent to a non-exported component as a capability token. Internally PendingIntents use a Binder token approach, so the only significant difference is the additional capability conferred by the PendingIntent to start a component.