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 usinggetIntent()
to retrieve theIntent
that was used to start up this activity in the first place. -
Craft an
Intent
that leads to theLoginActivity
, putting theIntent
from the previous step into anIntent
extra, then callstartActivity()
to bring upLoginActivity
. -
LoginActivity
, once the user successfully logs in, looks for thatIntent
extra. If it exists, it callsstartActivity()
on thatIntent
. If it does not exist, just do what you did before, and callstartActivity()
to bring upMainActivity
.
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.