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,
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
Intents 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
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
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
Notificationthat 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
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
Intentthat will take the user back to this same spot. You might be able to get away with just using
getIntent()to retrieve the
Intentthat was used to start up this activity in the first place.
Intentthat leads to the
LoginActivity, putting the
Intentfrom the previous step into an
Intentextra, then call
startActivity()to bring up
LoginActivity, once the user successfully logs in, looks for that
Intentextra. If it exists, it calls
Intent. If it does not exist, just do what you did before, and call
startActivity()to bring up
Life seems good. In reality, you have just giving an attacker a means of messing around with your app.
LoginActivity is blindly calling
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 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
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
LoginActivity is exported (by means of the
<intent-filter>). Hence, any app on the device
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,
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?
The researchers who wrote the white paper used this “next-intent” attack to gain control over a user’s Dropbox account:
LoginActivity attached the authentication
credentials to the “next”
Intent (as extras) before calling
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
LoginActivity can use as a regular
fragment and that other activities can use as a
blocking access until the user authenticates.
Another solution would be to make
LoginActivity not be exported.
Have a separate
LAUNCHER activity (e.g.,
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
LoginActivity with a nasty
Intent extra, keeping
as not-exported will block such access.
In general, if your code is accepting an
Intent or a
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
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.