Getting started with React Native security
2022年4月7日
0 分で読めますReact provides an easy and intuitive way to build interactive user interfaces. It lets you build complex applications from small, isolated pieces of code called components. React Native is an extension of React that enables developers to combine techniques used for web technologies like JavaScript with React to build cross-platform mobile apps.
This allows developers to write code once for multiple platforms, which speeds up development time. React Native uses a virtual dom to run application components and code on devices running Android, Android TV, iOS, macOS, tvOS, web, Windows, and UWP.
This level of adoption is great for React, as mobile technology is growing and evolving at an unprecedented speed. By 2025, there will be a projected 7.49 billion mobile users worldwide. Mobile apps are now ubiquitous and are becoming more sophisticated in sharing, handling, and securing user data.
As a developer building a user-oriented application, you are responsible for securing your app’s user information and keeping other sensitive server information secure. So, you must take steps to ensure that everyone sees only what they are authorized to access, and that user information stored in the database stays secure.
To help protect end user information, it is imperative that apps do not expose private keys, database information, sensitive files, or parts of the page that display the structure or operations of the back end. So let’s explore some security challenges that React Native developers encounter when developing mobile apps, like secure data transfer, authentication protocols, and dependency vulnerabilities. We’ll also cover ways to resolve each challenge.
Secure data transfer
It is hard to think of any mobile app that doesn’t require data from a remote server. By default, React Native uses only HTTPS for API calls and data transfers.
Although there are ways to override this default, using a non-HTTPS protocol will cause issues with your vendor when publishing to the app store. More seriously, it can expose your mobile app to various forms of data breaches.
You can manually pin the remote host’s trusted certificate(s) inside a mobile application to prevent man-in-the-middle (MITM) attacks and increase app security. There are a variety of suitable React Native open source certificate pinning libraries, like react-native-ssl-pinning by MaxToyberman or react-native-cert-pinner by Approov.
Pinning with the React Native SSL pinning library
React Native SSL pinning converts certificates to .cer
files that you can add as assets to your mobile app bundle, just as you’d add an image file or custom font.
The tool provides a swap-out for fetch
, which we can use to make our HTTP requests.
To pin a certificate, use this code:
1import { fetch } from 'react-native-ssl-pinning'
2
3const response = await fetch(url, {
4 method: 'POST',
5 body: body,
6 timeoutInterval: milliseconds,
7 headers: {
8 Accept: "application/json; charset=utf-8",
9 },
10 sslPinning: {
11 certs: ["cert1", "cert2"] // your certificates name (without extension), e.g. cert1.cer
12 }
13})
To pin a public key, use this code:
1import { fetch } from 'react-native-ssl-pinning'
2
3const response = await fetch(url, {
4 method: 'POST',
5 body: body,
6 timeoutInterval: milliseconds,
7 headers: {
8 Accept: "application/json; charset=utf-8",
9 },
10 pkPinning: true,
11 sslPinning: {
12 certs: ["sha256//r8udi/Ymw34stt44fZybMWq8YnFnIWXCqeHsTDRqy8=",
13 "sha256/QShfdvfff6fja30RrAn7JKnbQG/uEtLMkBgFF2Fuihg=",
14 ] // public keys
15 }
16})
The library uses two native modules under the hood: AFNetworking for iOS and okhttp3 for Android. The AFNetworking module automatically uses .cer in assets. OkHttp3 uses the JavaScript sslPinning
property to map to .cer
.
Pinning with react-native-cert-pinner
react-native-cert-pinner
is another library enabling you to pin certifications in your mobile app. Pins can be either the Base64 SHA256 hash of the certificate’s public key or the certificate’s Base64 SHA1 hash.
The library provides a utility, pinset, that generates an info.plist
file in iOS or GeneratedCertificatePinner.java
file in Android. The native code will then link to this file.
Under the hood, the library uses TrustKit on iOS and okhttp3 on Android.
To pin using react-native-cert-pinner
, first, generate a starter configuration using this command:
1$ npx pinset init
After initialization, determine which URLs to pin and each certificate's public key hash.
Then, enter your domain’s desired public key hashes into the pinset.json
file:
1{
2 "domains": {
3 "*.yourdomainhere.com": {
4 "pins": [
5 "sha256/0000000000000000000000000000000000000000000",
6 "sha256/1111111111111111111111111111111111111111111"
7 ]
8 },
9 // other domains
10 }
Note: Domains starting with an asterisk (*) will include all subdomains.
Now, generate the native project sources with the following command:
1$ npx pinset gen
This action should generate the respective files:
1Reading config file './pinset.json'.
2Updating java file './android/app/src/main/java/com/snyk/reactnative/GeneratedCertificatePinner.java'.
3Updating plist file './ios/example/info.plist'.
We recommend using a library that allows you to pin against the certificate’s public key since a pin created from a certificate will stop working when the certificate expires. Both libraries covered in this section allow public key pinning.
Generally, it is best practice to only pin certificates to a mobile app when you want to be relatively certain of a remote host's identity.
Authentication protocols
Authentication helps establish trust by identifying the particular user or system behind a network request. This identification helps the server establish whether it should allow the user to access a given resource.
Most mobile apps must show some form of customized data to the user. Consider a Facebook app profile or a WhatsApp contacts list. The authentication system helps the server identify the user to deliver the corresponding data in these apps.
A typical way to set up user authentication in React Native is to create different screen groups. Developers commonly achieve this using a router library like Stack Navigator.
One group manages the screens that unauthenticated users might encounter. These can include sign-in, registration, and password reset screens. The app uses these screens to authenticate the user.
A second group is for authenticated users. This stack includes dashboard, settings, account, feed, and any other screens related to user content.
This sample code splits screens into two groups: AppStack for authenticated users and AuthStack
for unauthenticated users:
1import {createStackNavigator} from '@react-navigation/stack';
2const Stack = createStackNavigator();
3
4const AppStack = () => {
5 return (
6 <Stack.Navigator>
7 <Stack.Screen name="Home Screen" component={HomeScreen} />
8 <Stack.Screen name="Notification Screen" component={NotificationsScreen} />
9 <Stack.Screen name="Profile Screen" component={ProfileScreen} />
10 <Stack.Screen name="Setting Screen" component={SettingsScreen} />
11 </Stack.Navigator>
12 );
13};
14
15const AuthStack = () => {
16 return (
17 <Stack.Navigator>
18 <Stack.Screen name="Sign In Screen" component={SignInScreen} />
19 <Stack.Screen name="Register Screen" component={RegisterScreen} />
20 <Stack.Screen name="Reset Password Screen" component={ResetPasScreen} />
21 </Stack.Navigator>
22 );
23};
After creating both screen groups, create a Router
component responsible for choosing which stack to show based on the user’s authentication status.
Then use both stacks inside the NavigationContainer
. A component from react-navigation
wraps all route navigations in React Native.
1import {NavigationContainer} from '@react-navigation/native';
2
3import {AppStack} from './AppStack';
4import {AuthStack} from './AuthStack';
5
6export const Router = () => {
7 return (
8 <NavigationContainer>
9 {authData ? <AppStack /> : <AuthStack />}
10 </NavigationContainer>
11 );
12};
authData
represents the data the user provides in the authentication screen — for example, SignInScreen
. This code authenticates the user:
1import React, {createContext, useState, useContext} from 'react';
2import {AuthData, authService} from '../services/authService';
3
4const AuthContext = createContext<AuthContextData>({} as AuthContextData);
5
6const AuthProvider: React.FC = ({children}) => {
7 const [authData, setAuthData] = useState<AuthData>();
8
9 const [loading, setLoading] = useState(true);
10
11 const signIn = async () => {
12 const _authData = await authService.signIn(
13 'test@email.com',
14 '12345678',
15 );
16
17 setAuthData(_authData);
18 };
19
20 const signOut = async () => {
21 setAuthData(undefined);
22 };
23
24 return (
25 <AuthContext.Provider value={{authData, loading, signIn, signOut}}>
26 {children}
27 </AuthContext.Provider>
28 );
29};
The signIn
function connects with the API and updates the authData
based on the response. You can store user profile data from the API there. The signOut
function clears the data. In some cases, it may connect with the API to invalidate a token.
A loading state is necessary because the authentication process is asynchronous and takes time to resolve. You can listen to this state to display a loading animation in your app’s user interface (UI) while the server is still processing the request.
Finally, ensure that all components can access the Context
by wrapping it with the AuthContext.Provider
.
The OAuth 2.0 protocol
React Native mobile applications commonly use the OAuth 2.0 authentication protocol to authenticate users. OAuth is a popular choice for web and mobile apps due to its stringent security.
The four actors in an OAuth flow are:
Resource Owner: Owns the data in the resource server. For example, a Facebook user is the Resource Owner of their Facebook profile data.
Client App: The application that wants to access this data (e.g., the Facebook mobile app)
Resource Server: The API that stores data the mobile application wants to access(e.g., the Facebook API)
Authorization Server: OAuth’s main engine
OAuth prompts users to authenticate via a third-party service such as Google, Facebook, or GitHub. Once the authentication process is complete, the third-party then redirects the user to the application with a verification code. The application can exchange this verification code for a JSON Web Token (JWT) or any other form of token.
These redirects are secure for web apps because URLs are guaranteed to be unique on the web. However, mobile apps do not have a centralized method of registering URL schemes.
To address this security concern, we must add a check in the form of PKCE (pronounced “Pixie”), which is an extension of the OAuth 2.0 spec. PKCE uses the SHA 256 cryptographic hash algorithm to verify that the authentication and token exchange requests come from the same client.
This approach ensures that only the application responsible for authorization flow can successfully exchange the verification code for a JWT.
A simple way to securely implement native OAuth in React Native is to use a package like react-native-app-auth. It serves as a React Native bridge for AppAuth for iOS and AppAuth for Android SDKs, which communicate with OAuth 2.0 and OpenID Connect providers.
The package supports PKCE and a host of OpenID and OAuth 2.0 providers, including Google, Okta, GitHub, Coinbase, and FusionAuth.
Scan for dependency vulnerabilities
Developers don’t build a fully fledged native application with React Native alone. We often use third-party tools to solve our problems and enhance our productivity.
An application contains several third-party dependencies that help make the app complete and functional. The problem is that these dependencies may introduce vulnerabilities to our application, even when our own code might be secure.
There are a few ways to get started using Snyk to scan for open source package vulnerabilities. Let’s look at an example using the popular open source React Native art app Eigen.
The first way is to use the Snyk CLI by installing the CLI tool and signing up for a free Snyk account. Once you are signed up and the tool is installed, run snyk auth
to connect the CLI to your account.
Clone the repository down to your development environment and then run snyk test
in the root directory of your cloned project. This will run the scan against the package manifest in the root and will look for any issues with the package versions.
Handy hint: You can also run snyk monitor
and be alerted of any issues that are detected within the project beyond the initial scan.
In our example, a number of potential issues have been detected in the react-native app package manifest. One thing that’s always handy here is that you can investigate the detected vulnerability to find further information.
Looking at the list, Hermes-engine 0.5.1 has a known vulnerability which can cause cross-site scripting (XSS). This can be mitigated by upgrading to a version greater than 0.7.2 that has addressed this particular vulnerability.
Another option for scanning React ative projects is to scan directly from a repository by connecting your Snyk account to your code repository of choice. The advantage here is that Snyk can do ongoing scans to detect issues within a project with multiple contributors, because the project is checked with each commit or merge.
To connect your repository, sign up for a Snyk account. Then, from the Snyk dashboard, click on the repository platform where your code is stored. If your platform of choice is not in the quick menu, click Other and search from the Snyk platform integrations.
Step through the connection process. Then, on the repository connection page, choose the repo you want to scan.
Click on the Add selected repositories button and wait for the scan to finish. What is really nice here is that you can view the scanned items and their findings.
Just as with the Snyk CLI scan, you can step through more detail around the detected vulnerability and raise an automatic fix back on the repository.
You can also integrate Snyk into your code editor of choice to automatically scan your dependencies for vulnerabilities as you develop an app. The Visual Studio Code Snyk extension, for example, will identify potential vulnerabilities not only within the package manifest but also where that package is called into the code.
Wrapping up
As mobile apps become more sophisticated in their data use, it is now even more critical to effectively safeguard user information and protect end users. Just like with web applications, React Native apps face the same type of security challengessuch as secure data transfer, authentication protocols, and dependency vulnerabilities.
Check out this checklist of React security best practicesto help you and your team find and fix security issues in your React applications.
Kingsley Ubah is an experienced full-stack web developer and technical writer.