Beware Accidental APIs: Avoid Intents as Extras

Earlier this year, I wrote a blog post warning developers about exposing broadcast receivers to third parties, as this creates an API that third parties might use, for good or ill.

However, the problem of accidental APIs is more general, and sometimes more subtle, than this. Screwing up what you allow third party apps to do can have some serious costs to you or your users.

A couple of weeks ago, Ars Technica wrote a post describing an academic paper that, in turn, documented violations of the “same-origin policy” with mobile apps. Alas, both are a bit terse on the nature of the problem, let alone any solution that does not involve modifying Android itself. In this post, I would like to dive into one of these scenarios a bit more deeply: using Intent s as extras.

You are writing an Android app that requires a login.

So, you create a LoginActivity , which has fields for a user ID and a password. You set that up to be the “front door” for your app, by giving that activity the standard MAIN / LAUNCHER <intent-filter> that puts an icon for LoginActivity in the home screen’s launcher. Now, when the user taps on the icon, they have to log in before proceeding. From there, you turn around and call startActivity() to bring up MainActivity , where you show your top-level content.

And life is good.

However, you eventually realize that the user might get into your application by other avenues:

They might click on a Notification that you elect to display

They might click on a link in a Web page to a URL that your application has elected to handle

They might use the recent-tasks list to come back to your app

In any of these cases, it is possible that their old login is now considered stale. Perhaps the process had been terminated, and therefore you do not have any login credentials. Perhaps the process had been around, but it has been a while since they were last in your app, and you want to re-authenticate them.

However, while you want to log them in again, you do not want to break the user intention. If they clicked on a link to go view some specific piece of content, after logging in, they should go to that specific piece of content, not to your top-level content as shown by MainActivity . Likewise, if they use the recent-tasks list and the system wants to return the user somewhere deep in your app (because that’s where they had last been), after logging in, you should take them to that point.

The solution that you might employ here is:

In the activity that is brought up by the Notification / URL / recent-tasks entry / whatever, detect that the user’s login credentials are stale or missing.

In that case, craft an Intent that will take the user back to this same spot. You might be able to get away with just using getIntent() to retrieve the Intent that was used to start up this activity in the first place.

Craft an Intent that leads to the LoginActivity , putting the Intent from the previous step into an Intent extra, then call startActivity() to bring up LoginActivity .

LoginActivity , once the user successfully logs in, looks for that Intent extra. If it exists, it calls startActivity() on that Intent . If it does not exist, just do what you did before, and call startActivity() to bring up MainActivity .

Life seems good. In reality, you have just giving an attacker a means of messing around with your app.

Specifically, LoginActivity is blindly calling startActivity() on the supplied Intent as an extra. In fact, if you went a bit more opaque and elected to wrap that Intent in a PendingIntent , and pass the PendingIntent as an extra, LoginActivity would have no choice but to blindly execute it. You cannot get at the underlying Intent of a PendingIntent to examine it, and therefore LoginActivity would have no means to “sanitize the input”, even if you thought to do that.

You are assuming that the extra is coming from one of your other activities. However, LoginActivity is exported (by means of the MAIN / LAUNCHER <intent-filter> ). Hence, any app on the device can call startActivity() on LoginActivity , have you log in, and then have you redirect to the activity of the other app’s choice.

Moreover, you are doing that from within your own process. Hence, even though MainActivity might not be exported, the third-party app could force you to open MainActivity . And the same holds true for any other private (non-exported) activity in your app.

At this point, you still might be wondering what the problem is. After all, displaying your activities is a good thing, right?

Not necessarily.

The researchers who wrote the white paper used this “next-intent” attack to gain control over a user’s Dropbox account:

For the Dropbox app, we exploited a private Activity VideoPlayerActivity, which has an input parameter “EXTRA_METADATA_URL” that specifies a URL from which to fetch the metadata for a video file. In a normal situation, this URL points to a file kept by dropbox.com. However, our next-intent exploit enables a malicious app to set the URL to arbitrary web domain, such as “http://attacker.com”. When the Dropbox app makes a request with that URL, it always assumes the recipient to be dropbox.com and attaches to the request an authentication header, as opposed to applying the conventional origin-based cookie policy. Since right now, EXTRA_METADATA_URL points to “http://attacker.com”, the adversary gets the header and can use it to gain a full access to the user’s Dropbox account.

Similarly, suppose LoginActivity attached the authentication credentials to the “next” Intent (as extras) before calling startActivity() . An attacker could simply have the “next” Intent point to the attacker’s own app, then read those extras and use them for malicious means.

One solution would be to not launch LoginActivity to have the user log in again. Instead, move the authentication behaviors to a LoginFragment , that LoginActivity can use as a regular fragment and that other activities can use as a DialogFragment , blocking access until the user authenticates.

Another solution would be to make LoginActivity not be exported. Have a separate MAIN / LAUNCHER activity (e.g., LauncherActivity ). This activity might not do much of anything, other than call startActivity() to bring up LoginActivity . However, since the “next-intent” exploit in this case requires that an attacker be able to directly invoke LoginActivity with a nasty Intent extra, keeping LoginActivity as not-exported will block such access.

In general, if your code is accepting an Intent or a PendingIntent as input (via an Intent extra, via an entry in a Bundle , via an AIDL parameter, etc.), you need to be sure that the only party who can send you that Intent is yourself. If third parties can craft that Intent or PendingIntent , it could be used against you.

The authors of the paper outlined a solution, to be added to Android itself, that would record the “origin” of any Intent objects, and therefore allow you to confirm that a received Intent really did originate with your own app, versus an attacker. This would be a nice improvement to Android, and perhaps we will see it someday.

In the interm, you need to ensure that all exported components sanitize their inputs, no matter what those inputs are. Blindly assuming that Intent inputs are valid may be hazardous to your user’s health.

Nervous about how the newest version of Android affects your app? Consider subscribing, then asking questions in the office hours chats!

— Sep 11, 2013