Android Nesting Intents
Does your app provide a service that starts another app’s Activity
as a callback when some action occurs? For example, does it accept an Intent
as an extra parameter of another Intent
, which is used as a parameter of a startActivity()
call?
Did you know this can leave your app vulnerable?
In the rest of this post, I’ll explain the problems of using this approach, and provide a solution that allows your app to provide the same functionality more safely.
The problem
The way we would expect this type of interaction to work would be something like this:
Here, the Client App creates an Intent
for its ClientCallbackActivity
and adds it as an extra to the Intent
that it will use to start the Provider App’s ApiService
. After processing the request, the Provider App uses the Intent
that the Client App provided to start the ClientCallbackActivity
.
❗️The thing to take note of here is that the Provider App is calling startActivity()
within its own app Context
. This has two consequences, both of which are suboptimal:
- Since
ClientCallbackActivity
is being started by Provider App, it must be marked asexported
, which allows not only Provider App to start it, but also any other apps on the device. - The nested
Intent
passed to ApiService can be used to start anyActivity
that’s part of Provider App. This included private, potentially sensitive, non-exported activitys!
To demonstrate, consider what would happen if the calling app didn’t provide an Intent
for its own activity, i.e.: ClientCallbackActivity
, but instead put in an Intent
to start a private activity within the Provider App.
Because a nested Intent
is used, it’s difficult for Provider App to guard against Intent
s that target private, potentially sensitive activities. Because Provider App is calling startActivity()
on the intent directly, it’s able to start ApiSensitiveActivity
even though it’s not exported.
The solution: PendingIntent
The solution is simple: instead of accepting an Intent
, Provider App could instead accept a PendingIntent
.
The difference between an Intent
and a PendingIntent
is that a PendingIntent
is always processed with the identity it was created with:
Because the callback is supplied as a PendingIntent
, when Provider App calls send()
on it, startActivity()
proceeds as if Attacker App had called it, and since the Attacker App doesn’t have privileges to start ApiSensitiveActivity
, the system prevents the activity from starting.
That’s certainly a benefit for Provider App, but what about our app, Client App? Well, since we’ve supplied a PendingIntent
, it’s now possible for ClientCallbackActivity
to be a private, non-exported Activity. The change has allowed better security for both apps!
If you’re familiar with the notification or alarm manager APIs, you’ll notice that they use PendingIntent
s for activating actions and alerting an app of an alarm. This is the reason the system uses PendingIntent
s, which are handled as the app that created them, rather than ordinary Intents.
Summary
Using Intent
as a mechanism to implement a callback to an Activity
can lead to vulnerabilities, both in the provider and client apps. This is due to the fact that Intents
are always processed in the Context
of the app they are called within. This Context
opens the possibility to start any non-exported activities in the provider app, and forces the client app to export the activity that should receive the callback.
PendingIntents
, by contrast, are always processed inside the Context
that created them. This not only allows a provider app to freely use them, without exposing non-exported activities, but also allows the client to specify any activity, including non-exported activities, to receive callbacks.
Learn more about how Android 12 is helping protect apps against unsafe launches of nested intents.