Written by Wisdom Ekpotu✏️
Recently, there has been a notable shift in mobile application development practices. Rather than creating separate applications for each native platform, many developers are opting for hybrid mobile frameworks like React Native.
The rationale behind this shift is compelling, yet it introduces its own unique set of challenges, especially concerning security. In this article, we'll explore some of the most prevalent security threats facing React Native mobile applications and discuss strategies for mitigating them.
Understanding React Native architecture
Before we dive into the specific security issues, let’s first understand how React Native works under the hood. For a long time, at a high level, a React Native app consisted of two main parts: the JavaScript thread and the native bridge. The JavaScript thread ran the JS code, while the native bridge communicated with the native platform (iOS or Android) APIs.
The bridge was also responsible for transferring data, events, and commands between the two domains. The bridge enabled the use of native modules and components, which are platform-specific code or UI elements that can be accessed from JavaScript.
However, the bridge also brought along its share of challenges and constraints:
- Asynchronicity: The bridge operates asynchronously, meaning data transfer between native and JavaScript threads isn't instantaneous. This can lead to UI inconsistencies or application logic delays
- Bottlenecking: The bridge acts as a bottleneck, constraining data transfer between the two threads based on bridge bandwidth and processing capabilities. This may impact application performance, particularly with extensive or intricate data structures
- Single point of failure (SPOF): The bridge represents a single point of failure; any malfunction or crash within it can bring the entire application to a halt. This could stem from bugs, memory leaks, or unhandled exceptions in native or JavaScript code.
To tackle these issues, React Native recently introduced enhancements and an alternative architecture. This revamped approach includes:
- JSI (JavaScript Interface): JSI is a new interface that allows direct access to the native objects and methods from the JavaScript code, without going through the bridge. JSI also allows the use of different JavaScript engines, such as Hermes or V8, instead of the default JavaScriptCore engine. JSI can improve the performance and stability of the applications, as well as enable new features, such as synchronous methods, shared memory, and multithreading. However, JSI is still experimental and not fully supported by all platforms and libraries
- TurboModules: TurboModules are a new way of implementing native modules that use JSI instead of the bridge. TurboModules can reduce the overhead and latency of the native module calls, as well as provide a more consistent and type-safe interface for the JavaScript code. It is important to note that TurboModules are still experimental and not fully supported by all platforms and libraries
- Fabric: Fabric is a new rendering architecture that uses JSI instead of the bridge. Fabric can improve the performance and responsiveness of the user interface, as well as enable new features, such as concurrent rendering, incremental rendering, and interactive animations. Fabric is also still experimental and not fully supported by all platforms and libraries
Now that you know what goes on under the hood, let's proceed.
Are React Native apps secure?
React Native apps are not inherently more or less secure than native apps. React Native apps still share the same security risks and challenges as native apps, such as data leakage, unauthorized access, malicious attacks, and compliance violations. However, React Native apps also have some unique security considerations, due to the use of JavaScript.
Does React Native inherit JavaScript vulnerabilities?
As we mentioned earlier, React Native apps are built using JavaScript and React, which means that they may inherit some of the vulnerabilities and limitations of JavaScript, the underlying language of the framework.
JavaScript is a dynamic and interpreted language, which means that it is prone to errors, bugs, and code injection attacks. JavaScript also lacks some of the security features and mechanisms of other languages, such as type safety, memory management, access control, etc. One of the most common and dangerous JavaScript vulnerabilities is cross-site scripting (XSS).
Are cross-site scripting (XSS) attacks possible?
Cross-site scripting (XSS) is a type of web-based attack that exploits the injection of malicious code into a web page or application. The attacker can use XSS to execute arbitrary code on the victim’s browser, steal cookies, session tokens, or other sensitive information, or redirect the user to a malicious website.
XSS is one of the most common and dangerous web vulnerabilities, and it affects both the server side and the client side of the web application. However, XSS is not a direct threat for React Native applications, as they do not run on a web browser, but on a JavaScript engine.
Therefore, the attacker cannot inject malicious code into the React Native application, as there is no HTML or DOM to manipulate. Moreover, React Native uses a virtual DOM to render the user interface, which prevents the execution of any inline or external scripts. However, this does not mean that React Native applications are immune to XSS attacks, as they may still interact with web-based content or services.
For example, React Native applications may use WebViews, OAuth 2.0, or deep links.
Securing WebViews
WebViews are components that allow React Native apps to embed a web browser within the app, using the native web engines of the platforms, such as WebView on Android and WKWebView on iOS. When not correctly implemented, WebWiews can expose some of the app's internal or sensitive information, such as cookies, headers, tokens, or URLs, to the web content or the web servers. This may result in data leakage, identity theft, or session hijacking.
To combat this issue, use the latest version of React Native WebView, which is the official and maintained module for WebViews in React Native.
Protecting OAuth 2.0 protocol and redirects
Another example of web-based interaction is the use of the OAuth 2.0 protocol for authentication and authorization. OAuth 2.0 is a standard protocol for authorization, which allows users to grant limited access to their resources from one site to another site without exposing their credentials.
OAuth 2.0 is widely used by many web and mobile applications, such as Facebook, Google, Twitter, etc., to enable users to sign in or sign up with their existing accounts on other platforms.
OAuth 2.0 also introduces some security risks. Firstly, OAuth 2.0 relies on redirects, which are mechanisms that send users from one URL to another URL, to complete the authorization flow. Redirects may be vulnerable to phishing, spoofing, or hijacking attacks, which may trick users into visiting malicious or compromised websites, or intercept the authorization tokens or codes.
Secondly, OAuth 2.0 requires the use of client IDs and client secrets, which are credentials that identify and authenticate the applications that use the protocol. Client IDs and client secrets may be exposed, leaked, or stolen, which may allow attackers to impersonate or abuse the applications, or access the users' resources without their consent.
Here is a way to prevent this:
- Use the react-native-app-auth library, which is a library that provides a secure and easy way to implement OAuth 2.0 and OpenID Connect (OIDC) in React Native apps. This module supports various authorization flows, such as authorization code, implicit, hybrid, etc., and various providers, such as Google, Facebook, Twitter, etc.
- Secure the client IDs and client secrets, which are credentials that identify and authenticate the applications that use the OAuth 2.0 protocol. Use libraries like react-native-keychain and react-native-secure-storage to encrypt or decrypt the client IDs and client secrets before using them in the OAuth 2.0 protocol
- Validate and verify the links and URLs that are used for deep linking, to ensure that they are safe and trusted
Getting deep linking right
Deep linking is a technique used to open specific screens or content within an app, using a URL or a link that is clicked by a user. It is useful for enhancing the user experience and the functionality of an app. Deep linking can also be used for authentication and authorization purposes, such as implementing OAuth 2.0 protocol and redirects, as discussed in the previous section.
Deep links are not secure and you should never send any sensitive information to them. Unprotected deep links may allow attackers to launch malicious or unwanted actions or requests within the app by crafting or manipulating the links or URLs that are clicked by the users.
To safeguard your deep links:
- Use the official React Navigation library, which is a cross-platform library that provides navigation and deep linking functionality for React Native apps
- If you intend on using third-party libraries, make sure that the libraries are not vulnerable to phishing attacks
- Use the deep linking config to specify the mapping between the links or URLs and the screens or content of the app, and avoid using the
path
prop, which can match any link or URL to the screen or content of the app - Validate and verify the links and the URLs that are used for deep linking
Man-in-the-middle (MITM) attacks
Man-in-the-middle (MITM) attacks are a type of attack that intercepts or alters the communication between two parties, such as the app and the server, or the app and the provider. MITM attacks can compromise the security and privacy of the communication, as they can access or manipulate the data, functionality, and session of the communication.
MITM attacks can also impersonate one or both parties and trick them into providing their credentials, personal information, or payment details. These attacks are common in network communication but they can also affect React Native apps.
React Native apps can use various methods and protocols to communicate with the server or the provider, such as HTTP, HTTPS, WebSocket, TCP, UDP, etc.: These methods and protocols can be vulnerable to MITM attacks if they do not properly encrypt, authenticate, or verify the communication. So, React Native developers need to take the following preventive measures:
- Use the HTTPS protocol for the communication, and avoid using the HTTP protocol, which can expose the communication to network sniffing or MITM attacks
- Use the SSL/TLS certificates to encrypt and authenticate the communication, and avoid using the self-signed or expired certificates, which can be invalid or untrusted
- Use the SSL pinning technique to verify the communication, and avoid using the default or system trust managers, which can accept any certificate
Implementing SSL pinning in React Native
SSL pinning is a technique that allows mobile apps to verify the identity of the server they are communicating with by checking that the server’s certificate matches a known value. SSL pinning can also help prevent man-in-the-middle attacks.
There are two main ways of implementing SSL pinning: certificate pinning and public key pinning. Both have their pros and cons but certificate pinning is the preferred choice because of its ease of implementation. Also, it's important to note that public key pinning is no longer supported by most browsers.
According to the React Native documentation:
When using SSL pinning, you should be mindful of certificate expiry. Certificates expire every 1-2 years and when one does, it’ll need to be updated in the app as well as on the server. As soon as the certificate on the server has been updated, any apps with the old certificate embedded in them will cease to work.
In React Native, you can achieve SSL pinning using react-native-ssl-pinning. This library implements React-Native SSL pinning and public key pinning using OkHttp 3 in Android, and AFNetworking on iOS.
Handling device security
Dealing with jailbroken and rooted devices
Jailbreaking and rooting are processes that allow users to gain full access to the operating system and the file system of their devices, which can enable them to install unauthorized apps, modify system settings, and bypass security restrictions. It is also good to note that emulators are also rooted.
Note: There are a lot of users that jailbreak their devices so, not allowing your apps to run on a jailbroken device could have a significant impact on your user base.
A good tool to detect jailbroken and rooted devices is the freeRASP for React Native library by Talsec.
Secure data storage in React Native
This is an important aspect of developing React Native apps, especially when dealing with sensitive information such as tokens, passwords, or payment details. You should avoid storing such data in plain text or using insecure storage solutions such as AsyncStorage
.
Instead, you should use a secure storage solution that encrypts the data at rest and protects it from unauthorized access.
Although iOS and Android provide tools to secure data stored on devices using Apple's Keychain and Android's Keystore respectively, these mechanisms make it more difficult to extract sensitive data from a device, but they should not be considered completely secure either as they can still be exploited (e.g., Apple Keychain exploit).
You can also make use of some RN wrappers around Keystone and Keystore such as:
- react-native-mmkv is a wrapper around MMKV that allows you to easily implement secure storage in your app. It is arguably the fastest key-value storage for React Native apps
- For those developers building React native apps using Expo, you can use the Expo SecureStore library. This module is easy to use and works similarly to
AsyncStorage
, but with added security. Currently, it has a size limit of 2048 bytes per value
Preventing screenshots
There are a few reasons why you might want to include this security feature when building your apps:
- For screens showing sensitive information like credit card info or password
- For screens that display paid content that you don't want to be available to the public
The following are some ways you can implement it.
Android developers have more direct control over screenshot prevention by using flags and permissions.
Navigate to /android/app/src/main/java/com/{Project_Name}/MainActivity.java
and add the following lines of code:
import android.os.Bundle;
import android.view.WindowManager;
...
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getWindow().setFlags(
WindowManager.LayoutParams.FLAG_SECURE,
WindowManager.LayoutParams.FLAG_SECURE
);
}
When it comes to iOS, things seem to become tricky as it goes against Apple's design. One way is to set an overlay screen in your AppDelegate.m
file. Consider this example:
- (void)applicationWillResignActive:(UIApplication *)application {
// fill screen with our own colour
UIView *colourView = [[UIView alloc]initWithFrame:self.window.frame];
colourView.backgroundColor = [UIColor whiteColor];
colourView.tag = 1234;
colourView.alpha = 0;
[self.window addSubview:colourView];
[self.window bringSubviewToFront:colourView];
// fade in the view
[UIView animateWithDuration:0.5 animations:^{
colourView.alpha = 1;
}];
}
- (void)applicationDidBecomeActive:(UIApplication *)application {
// grab a reference to our coloured view
UIView *colourView = [self.window viewWithTag:1234];
// fade away colour view from main view
[UIView animateWithDuration:0.5 animations:^{
colourView.alpha = 0;
} completion:^(BOOL finished) {
// remove when finished fading
[colourView removeFromSuperview];
}];
}
Also, for devs building with Expo, the Expo ScreenCapture plugin is available. Not only does the library prevent screenshots, but it also notifies if a screenshot has been taken while in the foreground.
App tampering and repackaging
App tampering and repackaging are types of attacks that involve the modification or alteration of the application code or content, either by the user or by a third party. These attacks are possible in React Native apps, as they use JavaScript and React, which are dynamic and interpreted languages, which means that they are easy to read or modify.
App tampering and repackaging can be performed by using reverse engineering or tampering tools, such as Apktool, dex2jar, etc.
App tampering and repackaging can be done for various purposes, such as:
- Cracking or pirating: The attacker can tamper or repackage the application to bypass the license or the payment verification, and use the application for free or distribute it to others
- Injecting or replacing: The attacker can tamper or repackage the application to inject or replace the code or the content with malicious or unwanted ones and perform harmful or fraudulent actions on the device or the application
- Spoofing or phishing: The attacker can repackage the application to spoof or mimic the appearance or behavior of the original application, and trick the user into providing their credentials or personal information
Here are two solutions that can be used to prevent app tampering:
Leverage the Google Play Integrity API
With this API provided by Google, you can run checks to make sure that requests and actions performed by users are actually coming from an uncorrupted/unmodified binary of your Android application.
You can also use it to know if your app is running on a genuine device. To learn more about this API, head over to the documentation.
Source code obfuscation
Source code obfuscation is a technique used to make easy-to-read code deliberately hard to understand and reverse engineer to prevent theft and modification.
Some options to consider are:
- Jscrambler helps harden your application's code while safeguarding your data from cyber threats. Some of the available features are polymophing obfuscation, self defending, and code locks
- JSDefender helps protect your JavaScript code from tampering, reverse engineering, and unauthorized debugging using various forms of obfuscation such as renaming, string encryption, resource encryption, etc.
- JavaScript obfuscator is an open source obfuscator for JavaScript that can be used for dead code injection, control flow flattening, strings extraction, and encryption
But you should note that obfuscation is not true security. At most, this approach will discourage some attackers but someone motivated enough can always reverse engineer the code — especially if an open source obfuscator is used, because it is known and de-obfuscators certainly already exist.
Another issue with this method is that obfuscation will make the code very difficult to interpret and optimize by the different JavaScript runtimes, resulting in a significant decline in the app's performance.
Security of third-party libraries
When working in React Native apps, you will usually depend on already written third-party packages to perform various tasks. This can pose a threat if any of those packages have a vulnerability. Hence we need to employ ways to scan for these vulnerabilities and ensure that all third-party packages we use are safe.
Here are some tools you can use to achieve this:
Conclusion
Securing your React Native applications is not a one-time task but an iterative process that evolves with your application and the broader security landscape. Mobile apps written with React Native can be well-protected by employing the various strategies outlined in this article.
Happy hacking!
LogRocket: Instantly recreate issues in your React Native apps
LogRocketis a React Native monitoring solution that helps you reproduce issues instantly, prioritize bugs, and understand performance in your React Native apps.
LogRocket also helps you increase conversion rates and product usage by showing you exactly how users are interacting with your app. LogRocket's product analytics features surface the reasons why users don't complete a particular flow or don't adopt a new feature.
Start proactively monitoring your React Native apps — try LogRocket for free.
Top comments (0)