Mastering the use of Android BroadcastReceiver
Introduction
Over the years and updates, the Android operating system has become more sophisticated with a bunch of new features. While this is a positive aspect for the end user, it can make application development more complex, increase the number of errors made by developers and lead to vulnerabilities. This is particularly true for Intent management and inter-process communication mechanisms. In fact, Android offers a set of tools enabling applications to communicate with each other. Among them, intents are the focus of attention since they enable activities, services and broadcasts to be started.
Thus, this blog post aims to present the BroadcastReceiver
component and see how it can be used in a secure way.
For this purpose, it will be structured in the following way :
- description of Intents;
- analyzis of the potential issues when broadcasting messages;
- analyzis of the potential issues when receiving broadcast messages;
- list and descriptions of the associated vulnerabilities.
Most code snippets used in this article are from the Android official documentation.
Intents
As briefly described in the introduction, Intents are very important in the Android ecosystem. They can be defined as high-level IPC abstraction elements used to deliver data to components across processes. According to the official Android developer documentation it could be represented as “a messaging object that can be used to request an action from another app component”. In order for the Android system to correctly route the request to the right recipient, it generally consists of the following elements:
- component name
- action
- data
- category
If you want to get more details please refer to the official documentation.
Explicit Intent
An explicit Intent specifies the application and/or the component that will be responsible for anwsering the request. It is commonly used to start components in your own app. As mentionned above, this is done by building the Intent with specific and recognizable elements like the app’s package name or a fully-qualified component class name.
The following lines show how to programmatically declare an explicit Intent and use it to start an activity:
Intent downloadIntent = new Intent(context, ConversationListActivity.class);
startActivity(downloadIntent);
Implicit Intent
Android is designed in such a way that it’s not necessary to specify the recipient of an Intent but instead just declare a general action to perform. Thus, you can just send an Intent across the system which further asks all applications and gathers those capable of answering. This behaviour is basically represented by the app switcher window, sometimes poping up on the user interface.
The following lines show how to programmatically declare an implicit Intent and use it to start an activity:
Intent sendIntent = new Intent();
sendIntent.setAction(Intent.ACTION_VIEW);
startActivity(sendIntent);
How to use BroadcastReceiver the right way
The BroadcastReceiver
operation is closely linked to the Intent object because the broadcast message has to be wrapped by it. Futhermore, BroadcastReceivers
operate on a sender/receiver model (which may be a single process or two different ones). This raises the question of how to protect this communication channel, so that only legitimate applications can send or receive messages.
Thus, before starting the development process, one needs to think about who will be the sender and the receiver.
As the sender
Sending a broadcast message is done programmatically as follows, using the sendBroadcast
or sendOrderedBroadcast
functions:
Intent intent = new Intent();
intent.setAction("com.example.broadcast.MY_NOTIFICATION");
intent.putExtra("data", "Nothing to see here, move along.");
sendBroadcast(intent);
In the example above, the broadcast message will be sent, without any particular restriction, to all applications. Then, only those that have declared a receiver whose filter corresponds to the action MY_NOTIFICATION
will be able to respond.
<receiver android:name=".MyBroadcastReceiver" android:exported="true">
<intent-filter>
<action android:name="com.example.broadcast.MY_NOTIFICATION" />
</intent-filter>
</receiver>
At this time, careful readers may notice that sensitive data may be sent to unintended recipients. The associated vulnerability is called Implicit Intent Interception and will be detailed in another section.
Authenticate the receiver
There are two ways to protect a broadcast message and ensure that only legitimate recipients receive it.
Use a permission
This involves improving the use of the sendBroadcast
or sendOrderedBroadcast
functions, which now take a specific permission (builtin or custom) as a parameter.
Intent intent = new Intent();
intent.setAction("com.example.broadcast.MY_NOTIFICATION");
intent.putExtra("data", "Nothing to see here, move along.");
// Adding specific permission
sendBroadcast(intent, <permission_name>)
From now on, the receiver must declare this permission in its Manifest and get it granted at runtime (otherwise, the message will not be delivered).
<uses-permission android:name="<permission_name>"/>
Please note, however, that the protection level associated with the custom permission must be set to signature so that only apps that are signed with the same key can use them. Other protection levels (normal or dangerous) do not offer the expected security benefits as any application can ask for permissions protected with them.
Use an explicit Intent
Using an explicit Intent allows you to specify the target component and its class:
Intent intent = new Intent();
// Using an explicit Intent
intent.setClassName("com.example", "com.example.myclass");
context.sendBroadcast(intent);
The broadcast message will then only be delivered to the specified component and malicious applications will no longer be able to intercept it.
As the receiver
The Android operating system offers two types of receivers, not behaving the same way. In fact, receiving a broadcast message can be done using a context-registered
or a manifest-declared
one.
First and foremost, both need to be associated with a class which extends the BroadcastReceiver
Java API one :
public class MyBroadcastReceiver extends BroadcastReceiver {
private static final String TAG = "MyBroadcastReceiver";
@Override
public void onReceive(Context context, Intent intent) {
// your code here
}
}
The onReceive
function is then triggered each time a broadcast message is received.
Context-registered
Context-registered BroadcastReceivers
are programmatically registered in the following way:
BroadcastReceiver br = new MyBroadcastReceiver();
IntentFilter filter = new IntentFilter("android.intent.action.SAMPLE");
// Defining flags to restrict export
boolean listenToBroadcastsFromOtherApps = false;
if (listenToBroadcastsFromOtherApps) {
receiverFlags = ContextCompat.RECEIVER_EXPORTED;
} else {
receiverFlags = ContextCompat.RECEIVER_NOT_EXPORTED;
}
// Using one of this flag in the registerReceiver method
ContextCompat.registerReceiver(context, br, filter, receiverFlags);
They have special features:
- can only receive implicit Intents;
- can receive messages as long as their registration context is valid (the process is running);
- are automatically exported before Android 13.
WARNING : The “flag” feature presented in the above code snippet is only available since Android 13.
Manifest-declared
As their name suggest, manifest-declared receivers are declared in the AndroidManifest.xml
file:
<receiver android:name=".MyBroadcastReceiver">
<intent-filter>
<action android:name="android.intent.action.SAMPLE"/>
</intent-filter>
</receiver>
Please also note that unlike their context-registered counterparts, messages can be received even if the application is not launched (the Android system is able to start it).
In both examples above, any sender can broadcast a message to the application as there is no specific control. The associated vulnerability is called unprotected exported receiver and will be detailed in another section.
Authenticate the sender
To authenticate the sender, we must include a specific permission in the manifest file or directly in the code in case of context-registered receivers.
When dealing with manifest-registered receivers, a specific android:permission
has to be added in the AndroidManifest.xml
declaration :
<receiver android:name=".MyBroadcastReceiver"
android:permission="android.permission.BLUETOOTH_CONNECT">
<intent-filter>
<action android:name="android.intent.action.SAMPLE"/>
</intent-filter>
</receiver>
On the other hand, for context-registered ones, a new parameter must be added in the registerReceiver
method :
BroadcastReceiver br = new MyBroadcastReceiver();
IntentFilter filter = new IntentFilter("SAMPLE");
// Adding permission
ContextCompat.registerReceiver(context, br, filter, permission);
This way, only senders having been granted the permission will be able to send a broadcast message to this receiver.
Associated vulnerabilities
Implicit intent interception
Problem: unrestricted receiver
Here, the issue stems from a broadcast message being sent to unrestricted receivers (with an implicit intent). Therefore, the Intent object’s data can be intercepted by a malicious application having declared a receiver matching the same action.
Let’s consider the following code (gotten from oversecured) which sends a broadcast message containing extra data (id and text) to all applications :
Intent intent = new Intent("com.victim.messenger.IN_APP_MESSAGE");
intent.putExtra("from", id);
intent.putExtra("text", text);
sendBroadcast(intent);
Any application on the system declaring the following receiver can intercept the Intents’ data:
<receiver android:name=".EvilReceiver">
<intent-filter>
<action android:name="com.victim.messenger.IN_APP_MESSAGE" />
</intent-filter>
</receiver>
This data can further be logged (like in the code snippet below) but also sent to a remote server.
public class EvilReceiver extends BroadcastReceiver {
public void onReceive(Context context, Intent intent) {
// Checking Intent action
if ("com.victim.messenger.IN_APP_MESSAGE".equals(intent.getAction())) {
// Logging extra data (id and text)
Log.d("evil", "From: " + intent.getStringExtra("from") + ", text: " + intent.getStringExtra("text"));
}
}
}
This underlines the ease of exploitation (no privileges required) and the consequences this could have.
Unprotected exported receiver
Problem: unrestricted sender
Here, the issue stems from an unprotected exported receiver handling data from untrusted sources. As an example, please refer to the InsecureShop app’s vulnerable source code.
The AboutUsActivity
class declares the following exported context-registered receiver.
class AboutUsActivity : AppCompatActivity() {
lateinit var receiver: CustomReceiver
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_about_us)
// Get receiver object
receiver = CustomReceiver()
// Register receiver with CUSTOM_INTENT action
registerReceiver(receiver, IntentFilter("com.insecureshop.CUSTOM_INTENT"))
}
}
The CustomReceiver
class (triggered when a broadcast message is received) is then declared as follows :
class CustomReceiver : BroadcastReceiver(){
override fun onReceive(context: Context?, intent: Intent?) {
val stringExtra = intent?.extras?.getString("web_url")
if (!stringExtra.isNullOrBlank()) {
val intent = Intent(context, WebView2Activity::class.java)
intent.putExtra("url",stringExtra)
context?.startActivity(intent)
}
}
}
The onReceive
function is retrieving an URL as an extra parameter before sending it to a WebView
component.
From the attacker’s point of view, this can be abused to redirect the user to a malicious website by sending a specifically crafted Intent containing a malicious URL as an extra parameter.
Improper verification of an Intent by the BroadcastReceiver
Problem: unverified action
Here, the issue stems from an unvalidated user-controlled data.
On Android, when an app registers an exported component and adds it to an intent-filter
, that component can simply be started with an explicit Intent, regardless of its filter. This could lead to a security issue, especially when using the operating system’s BroadcastReceiver
which can be triggered by any third-party app using an explicit Intent.
As an example, let’s take the ACTION_BOOT_COMPLETED
receiver :
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="test">
<application>
<receiver android:name=".BootReceiverXml">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
</application>
</manifest>
The associated Java class with its onReceive
function is declared as follows :
public class ShutdownReceiver extends BroadcastReceiver {
@Override
public void onReceive(final Context context, final Intent intent) {
// does not verify Intent action
mainActivity.saveLocalData();
mainActivity.stopActivity();
}
}
Those few lines (gotten from codeql) are vulnerable because they do not verify the Intent action before executing the onReceive
function. As a result, any explicit Intent sent to this component will be able to trigger the saveLocalData
and stopActivity
functions.
Please note that this vulnerability has been addressed in Android 13 but only apps which targets SDK 34 (
targetSDK
) will be protected.
Conclusion
Throughout this blog post we fully demonstrated the necessity to think about who is allowed to send and/or receive broadcast messages using Android’s BroadcastReceiver
component. Even beyond this component, Android’s inter-process communication (IPC) mechanisms are full of possibilities that could be misimplemented.
Now, more than ever, it is crucial to conduct external audits to ensure the absence of any flaws in your implementation.
We at Stackered can assess the security of your mobile applications (Android & iOS) and accurately identify the attack surface and hot spots of your product.