Investigating and Mitigating Security Risks in a React Native App 

A recent static application security testing (SAST) scan flagged a finding in our Android app: “Context Registered Broadcast Receivers Not Protected with Permissions.” After a thorough investigation, we concluded that the finding is valid, but has minimal impact in the current implementation. In this blog post, we walk through our investigation process and explain why we patched the broadcast receiver even though we ultimately did not classify it as a product vulnerability. 

Background 

Before we jump into the investigation, we need to lay some groundwork on the Android ecosystem and React Native. This is by no means a comprehensive guide, but it provides the context for this investigation. 

Android Architecture 

The four foundational components of an Android application are activity, service, broadcast receiver, and content provider: 

  • Activities arguably play the most important role in an Android application. An activity is a screen within an app that provides a user interface. Activities enable users to interact with the app’s content and functionality. 
  • Services run in the background; therefore, unlike activities, they do not have associated screens within the app. Services allow tasks to run while the app is not in use, such as playing music, performing downloads, tracking location, etc. 
  • Broadcast receivers listen for and react to system-wide events such as battery state changes, network connectivity events, system boot completion, as well as custom application events. Broadcast receivers can be manifest-declared or context-registered. Developers statically declare manifest-declared broadcast receivers in the app’s manifest, and they can respond to events both when the app is running and not running. Developers dynamically register context-registered broadcast receivers during runtime, and they only work while the context that registered them is active (activity, service, etc.). 
  • Content providers allow access to a structured set of data, and apps can use them within the app or share them across other applications. Content providers are the least common of the four foundational Android components. 

Intents and Intent Filters play a key role in the Android ecosystem. An intent describes an action to be performed (e.g. starting an activity or sending a broadcast), and an intent filter defines the types of Intents a component can respond to. 

With the Android pieces in mind, here’s how React Native fits on top. 

React Native Architecture 

React Native is a popular framework for building native mobile applications using React. It enables developers to use (mostly) a single codebase to produce an application that renders truly native UI components. When needed, JavaScript can call into native code written in platform-specific languages like Kotlin for Android or Swift for iOS. 

In contrast, native mobile development uses platform-specific languages to create fully native applications and typically does not allow any code sharing. 

Let’s now dive into the initial phase of the investigation, the static analysis. 

Static Analysis with MobSF 

You can test mobile apps with SAST in two ways: through the codebase or by analyzing the build artifact (such as an IPA file on iOS or an APK on Android). Performing SAST on a build artifact offers additional benefits over analyzing custom code alone by providing insight into how the code interacts with third-party dependencies and the build processes, uncovering vulnerabilities that may only manifest in the compiled application. If you do not have access to the codebase, analyzing the build artifact may be your only option. 

MobSF is one of the most popular tools for static analysis of mobile binaries, and we used it for this investigation. You can use MobSF to understand high-level functionality and capabilities, look at configuration files, find strings such as secrets and URLs, and view and search through decompiled source code.  

After we used MobSF to search the decompiled code for broadcast receivers, we discovered the culprit: a third-party library, @voximplant/react-native-foreground-service. On Android Oreo (API level 26) and up, the receiver is registered with flags=2 (Figure 1), which the Android documentation maps to RECEIVER_EXPORTED (Figure 2), meaning the receiver is exported. 

After diving deeper into the Mattermost codebase to see how it uses the library, we determined that the Calls feature runs a foreground service to ensure that a call can continue even when the app is in the background. Using a foreground service as opposed to a regular service ensures that the Android operating system does not unexpectedly kill the service. This guarantees that calls do not unexpectedly get dropped. One key difference with a foreground service is that it displays a status bar notification to indicate to the user that the app is running. In the @voximplant/react-native-foreground-service library, the foreground service registers an exported broadcast receiver on start to handle intents (i.e., events) with action FOREGROUND_SERVICE_BUTTON_PRESSED when a user presses the status bar notification button. 

The library then propagates the event to the JavaScript layer, which triggers any registered event handlers. Because the library registers the broadcast receiver as exported, any app, including malicious ones, can trigger the event. 

After having gained enough context from static analysis, we switched over to dynamic analysis to understand what truly happens at runtime. 

Dynamic Analysis with drozer 

Traditional Web/API vulnerability scanners, which are a type of DAST product, typically don’t require access to the running application or server. Mobile DAST requires a device (physical or virtual) with the installed application to analyze the runtime behavior. 

drozer is a security testing framework for Android applications with two key components — the drozer client and the drozer agent. The drozer agent is an Android application that runs on the target device, and the drozer client is a CLI tool that runs on your desktop of choice. The drozer client issues commands to the drozer agent. drozer has a variety of powerful capabilities and allows one to emulate a malicious application, often bypassing the need to write and deploy a custom app. 

Next, we used the intent filter information gathered during the SAST phase to craft a broadcast to the Mattermost app after initiating a Call and putting the app in the background. 

The result: only a log was produced, no other behavior was triggered. 

These results show that a malicious application could interact with the Mattermost app, although no handlers actively processed the received event, confirming current implementation posed no immediate risk of exploitation. After we performed further manual analysis on the codebase, we determined that the JavaScript layer emitted the log because the app had not registered any event handler for the event. 

If the app had registered any event handlers, it would have executed them. Given the context of this feature, one can easily imagine a scenario where the event could close the foreground service and terminate the call, allowing an attacker to perform a denial-of-service (DoS). 

Now that the full picture was painted, we dove into remediating the issue. 

The Fix 

Although the issue produced only a log with no other side-effects, we decided to still patch it. This adjustment serves as an important safeguard, mitigating the risk that a developer might introduce an event handler in the future that another application could unknowingly trigger. We added a patch to ensure the broadcast receiver is not exported on Android versions supporting the RECEIVER_NOT_EXPORTED flag, which prevents third-party applications from broadcasting events to the broadcast receiver. This mitigates potential vulnerabilities stemming from changes in app behavior or third-party integrations. We first shipped the change in v2.29.0 of the Android application. 

With the patch shipped, here’s a concise recap of what changed and why. 

Summary 

To summarize, @voximplant/react-native-foreground-service is a third-party dependency that enables React Native developers to leverage foreground services in their Android application. When the service starts, it registers an exported broadcast receiver that listens for the “FOREGROUND_SERVICE_BUTTON_PRESSED” broadcast. When it receives this broadcast, it sends an event using (sendEvent()) to the JavaScript layer, where the JavaScript listens for events and either triggers any registered event handlers or logs that no event handlers exist. 

Mattermost uses this library for the calls feature in the Android app, ensuring that calls remain active even when the app runs in the background. Currently, the Mattermost app does not register any handlers for the “FOREGROUND_SERVICE_BUTTON_PRESSED” event. However, because the broadcast receiver is exported by default, Mattermost applies a custom patch to this library to mark the broadcast receiver as non-exported. This adjustment serves as an important safeguard, mitigating the risk that a developer might introduce an event handler in the future that another application could unknowingly trigger. 

This investigation underscores the importance of proactive security strategies to safeguard applications as features and dependencies evolve. 

Lessons Learned 

Several lessons can be learned from this endeavor: 

  1. Prioritize defense-in-depth strategies to prepare for future challenges. 
  2. Use DAST and manual analysis to complement SAST and SCA results. 
  3. Thoroughly review third-party dependencies to mitigate unforeseen risks.
  4. Distinguish between a bug and a vulnerability to support risk management and business processes.

Additional Resources