Android Intent security vulnerabilities

Petros Douvantzis
6 min readApr 20, 2022

Intents are a primary element of Android architecture. They can be used used to start activities, services, enable IPC (Inter-process communication), pass messages from one part of the app to the other, enable functionality via notifications (such as inline-reply in a messaging app) etc.

If not implemented correctly, they can enable other apps to read information you though was private or let a malicious app affect your app’s functionality. You would be surprised to find out that most probably one or both of these issues can be exploited in your app.

This article will focus on broadcast intents and broadcast receivers and assumes that you are familiar with basic usage. It will tap into the topics explained in Google’s excellent article and dive a bit deeper: https://developer.android.com/guide/components/broadcasts

The 2 questions/requirements

When implementing a broadcast-receiver scheme, there are 2 questions you should ask yourself:

  1. Do you know who the receiver(s) of your broadcast is? In other words, can an unwanted app be listening to your broadcasts?
  2. Do you know who the sender(s) of a received broadcast is? In other words, can an unwanted app be sending you broadcasts?

We will try to see how the different types of broadcast receivers and setups can affect the outcome of these 2 questions.

Broadcast receiver types

There are 2 types of broadcast receivers: manifest-declared receivers and context-registered receivers.

A context-registered receiver has the advantage that you can programmatically register it and unregister it. Your app should be running for the receiver to work. It can be used only for subscribing to implicit intents. It is always exported (and possibly accessed by other apps).

A manifest-declared receiver will always receive updates. Android can even launch your app so that your receiver gets the broadcast. Starting from Android 8, you can’t use such a receiver for implicit intents. It can be used only for subscribing to explicit intents. It can be declared as exported or non exported.

A naive implementation

The simplest way to send an intent is creating an implicit intent. Even though Google encourages the use of your package name, the string of the intent can literally be whatever you want.

Intent intent = new Intent();
intent.setAction("com.whatever.SOME_NOTIFICATION");

On the receiving side, you will create a context-register receiver using

IntentFilter filter = new IntentFilter("com.whatever.SOME_NOTIFICATION");

Even though this can work, it fails on both of the requirements stated earlier. A third party-app can also register to this intent, if it knows its name. Also, a third-party app can broadcast this intent, resulting on your app receiving it.

Here are some possible solutions:

LocalBroadcastManager

If your intent (pun intended) is to pass messages from one part of your app to the other, you should simply use LocalBroadcastManager. It’s a simpler implementation that doesn’t involve IPC and thus is secure and lightweight.

You can dynamically register or unregister a receiver. Ordered broadcasts are not supported unfortunately out of the box, but you can find implementations that work around this.

Non exported receiver

Declare a receiver in the manifest (and implement the receiver class).

<receiver android:name=".SomeReceiver" />

Broadcast an explicit intent to that receiver:

Intent intent = new Intent();
intent.setClassName("com.yourpackagename.yourappname", "com.yourpackagename.yourappname.SomeReceiver");

This way there can be only one receiver (we met the first requirement). However, another app can still send a broadcast to your receiver if it knows the package name and class name.

A simple solution to that, is to define `android:exported=”false”` to your receiver declaration in the manifest.

<receiver android:name=".SomeReceiver" 
android:exported="false"/>

This way we have also met the second requirement.

This setup is useful for creating PendingIntents to pass for example to the AlarmManager or to a Notification. The only downside to this approach is that you have no way of unsubscribing. You can work around this by using LocalBroadcastManager to forward the intent from your receiver class to the actual place it needs to be delivered to.

Permissions

If you want to have a way to send broadcasts between two apps that you own, the previous answers will be of no use. Using permissions may help you in this case. The permission can use `android:protectionLevel=”signature”` so that only the apps that are signed with the same signature, can use them. Declaring a permission without a protection level does not offer any security benefits.

In the following paragraphs, we won’t go through the exact way to use permissions as you can read that in the linked Android article above. Instead, we will go in depth into the implications of using permissions.

Sending with permissions

The sending side includes programmatically the name of the permission when sending the broadcast. It can be an implicit or explicit intent.

The receiving side uses the permission in its manifest. It can use a manifest-declared (for explicit intents) or context-registered (for implicit intents) receiver.

Only apps that use this permission in their manifest will have their receivers triggered by our broadcast. However, these receivers can also be triggered by broadcasts that don’t use the permission.

So, when sending with permission, we know who the receiver will be. We don’t know who the sender(s) will be.

This method fulfills the first requirement but fails on the second one.

Receiving with permissions

The sending side uses the permission in its manifest. It can send an implicit or explicit intent (without specifying the permission name when sending).

The receiving side includes the permission programmatically in its context-registered receiver or when declaring its manifest-based receiver.

Only senders that use the permission can send to our receiver. However, receivers of other apps will be able to receive the broadcast of the sender.

So, when receiving with permission, we know who the sender will be. We don’t know who the receiver(s) will be.

This method fulfils the second requirement correctly but fails on the first one.

Depending on your use case, this may be enough. If you use an explicit intent and “receiving with permissions”, you can manage to fulfil both requirements.

Receiving and sending with permissions in 1 way communication

This is not mentioned in Android’s article, but you can combine the two previous methods.

The sending side uses the permission in its manifest. It includes programmatically the name of the permission when sending the broadcast.

The receiving side uses the permission in its manifest. It includes the permission programmatically in its context-registered receiver or when declaring its manifest-based receiver.

By sending and receiving with permissions, we know who the sender will be and who the receiver will be.

Receiving and sending with permissions in 2 way communication

In the previous scenario there was a sending app and a receiving app. In this scenario, both apps are able receive and send broadcasts in a secure manner.

Both apps use the permission in their manifest. The receiving side (of each app) includes the permission programmatically in its context-registered receiver or when declaring its manifest-based receiver. The sending side (of each app) includes programmatically the name of the permission when sending the broadcast.

Installation order

Note that when using a permission, the app installation order matters.

The app that declares the permission in its manifest (it can be the receiver, the sender or even a third-party app), has to be installed before the app that uses the permission in its manifest.

Two apps can declare the same permission if they have the same signature. Otherwise, the second app will fail to install.

Conclusion

According to your use case and restrictions, you can make use of some of the methods explained. In either case, you should always try to check if your implementation meets the 2 requirements:

  • Unwanted apps should not be able to listen to your broadcasts
  • Unwanted apps should not be able to send broadcasts to your app

And don’t stay in theory but test it as well! Create a testing app that tries to receive your broadcast or send a broadcast to your app.

--

--