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.