Skip to main content

React Nativeセキュリティ入門

著者

Kingsley Ubah

blog banner node js

2022年4月7日

0 分で読めます

Reactを使うと、インタラクティブなユーザーインターフェースを簡単かつ直感的に構築できます。コンポーネントと呼ばれる小さく独立したコード単位を組み合わせて、複雑なアプリケーションを開発できます。React NativeはReactを拡張したもので、JavaScriptなどのWeb技術で使われる手法とReactを組み合わせ、クロスプラットフォームのモバイルアプリを構築できます。

これにより、開発者は一度コードを書くだけで複数のプラットフォームに対応でき、開発期間を短縮できます。React Nativeは仮想DOMを使用して、Android、Android TV、iOS、macOS、tvOS、Web、Windows、UWPを搭載したデバイス上でアプリケーションのコンポーネントとコードを実行します。

モバイル技術がかつてない速さで成長・進化している今、Reactが広く採用されているのは喜ばしいことです。2025年までに、世界のモバイルユーザー数は74億9,000万人に達すると予測されています。モバイルアプリは今や至るところにあり、ユーザーデータの共有、取り扱い、保護において、ますます高度化しています。

ユーザー向けアプリケーションを開発する開発者には、アプリのユーザー情報を保護し、その他の機密性の高いサーバー情報も安全に保つ責任があります。そのため、すべてのユーザーが許可された情報にのみアクセスできるようにし、データベースに保存されたユーザー情報を安全に保つ必要があります。

エンドユーザーの情報を保護するには、秘密鍵やデータベース情報、機密ファイル、バックエンドの構造や処理を示すページの一部などをアプリから露出させないことが不可欠です。そこで、モバイルアプリの開発時にReact Nativeの開発者が直面する、データの安全な転送、認証プロトコル、依存関係の脆弱性などのセキュリティ課題を見ていきましょう。それぞれの課題への対処方法も解説します。

データを安全に転送する

リモートサーバーからデータを取得しないモバイルアプリを考えるのは難しいでしょう。React Nativeでは、デフォルトでAPI呼び出しとデータ転送にHTTPSのみを使用します。

このデフォルト設定を上書きする方法はありますが、HTTPS以外のプロトコルを使用すると、アプリストアへの公開時にベンダーとの問題が生じます。さらに深刻なのは、さまざまな形のデータ侵害にモバイルアプリがさらされる可能性があることです。

モバイルアプリ内でリモートホストの信頼できる証明書を手動でピン留めすると、中間者(MITM)攻撃を防ぎ、アプリのセキュリティを強化できます。React Nativeで使えるオープンソースの証明書ピンニングライブラリには、MaxToybermanのreact-native-ssl-pinningやApproovのreact-native-cert-pinnerなどがあります。

React Native SSL pinningライブラリを使ったピンニング

React Native SSL pinningは、証明書を.cerファイルに変換します。画像ファイルやカスタムフォントと同じように、モバイルアプリのバンドルにアセットとして追加できます。

このツールはfetchの代わりに使える機能を提供しており、HTTPリクエストの送信に利用できます。

証明書をピン留めするには、次のコードを使用します。

import { fetch } from 'react-native-ssl-pinning'

const response = await fetch(url, {
  method: 'POST',
  body: body,
  timeoutInterval: milliseconds,
  headers: {
    Accept: "application/json; charset=utf-8",
  },
  sslPinning: {
    certs: ["cert1", "cert2"] // your certificates name (without extension), e.g. cert1.cer
  }
})

公開鍵をピン留めするには、次のコードを使用します。

import { fetch } from 'react-native-ssl-pinning'

const response = await fetch(url, {
  method: 'POST',
  body: body,
  timeoutInterval: milliseconds,
  headers: {
    Accept: "application/json; charset=utf-8",
  },
  pkPinning: true,
  sslPinning: {
    certs: ["sha256//r8udi/Ymw34stt44fZybMWq8YnFnIWXCqeHsTDRqy8=",
        "sha256/QShfdvfff6fja30RrAn7JKnbQG/uEtLMkBgFF2Fuihg=",        
      ] // public keys
  }
})

このライブラリは、内部で2つのネイティブモジュールを使用します。iOSではAFNetworking、Androidではokhttp3です。AFNetworkingモジュールは、アセット内の.cerファイルを自動的に使用します。OkHttp3では、JavaScriptのsslPinningプロパティを使って.cerファイルを指定します。

react-native-cert-pinnerを使ったピンニング

react-native-cert-pinnerも、モバイルアプリで証明書をピン留めできるライブラリです。ピンには、証明書の公開鍵のBase64 SHA256ハッシュ、または証明書のBase64 SHA1ハッシュを使用できます。

このライブラリにはpinsetというユーティリティがあり、iOS用のinfo.plistファイル、またはAndroid用のGeneratedCertificatePinner.javaファイルを生成します。その後、ネイティブコードがこのファイルを参照します。

このライブラリは、内部でiOSではTrustKit、Androidではokhttp3を使用します。

react-native-cert-pinnerでピンニングするには、まず次のコマンドで初期設定を生成します。

$ npx pinset init

初期化したら、ピン留めするURLと、それぞれの証明書の公開鍵ハッシュを確認します。

次に、対象ドメインの公開鍵ハッシュをpinset.jsonファイルに入力します。

{
  "domains": {
    "*.yourdomainhere.com": {
      "pins": [
        "sha256/0000000000000000000000000000000000000000000",
        "sha256/1111111111111111111111111111111111111111111"
      ]
    },
   // other domains
  }

注: アスタリスク(*)で始まるドメインは、すべてのサブドメインを含みます。

次のコマンドを実行して、ネイティブプロジェクトのソースを生成します。

$ npx pinset gen

この操作により、次のファイルが生成されます。

Reading config file './pinset.json'.
Updating java file './android/app/src/main/java/com/snyk/reactnative/GeneratedCertificatePinner.java'.
Updating plist file './ios/example/info.plist'.

証明書自体から作成したピンは証明書の有効期限が切れると機能しなくなるため、証明書の公開鍵をピン留めできるライブラリの使用をおすすめします。このセクションで紹介したライブラリは、どちらも公開鍵のピンニングに対応しています。

一般に、リモートホストの身元を確実に確認したい場合に限り、モバイルアプリで証明書をピン留めするのがベストプラクティスです。

認証プロトコル

認証は、ネットワークリクエストを送信したユーザーやシステムを特定することで信頼を確立します。この特定により、サーバーはユーザーに特定のリソースへのアクセスを許可するかどうかを判断できます。

ほとんどのモバイルアプリでは、ユーザーごとにカスタマイズされたデータを何らかの形で表示する必要があります。FacebookアプリのプロフィールやWhatsAppの連絡先リストを考えてみてください。認証システムは、サーバーがユーザーを特定し、これらのアプリで対応するデータを配信できるようにします。

React Nativeでユーザー認証を設定する一般的な方法は、画面を複数のグループに分けることです。開発者は通常、Stack Navigatorのようなルーターライブラリを使ってこれを実現します。

一方のグループでは、未認証のユーザーが利用する画面を管理します。ログイン、登録、パスワードリセットなどの画面が含まれます。アプリはこれらの画面を使ってユーザーを認証します。

もう一方のグループは、認証済みユーザー向けです。このスタックには、ダッシュボード、設定、アカウント、フィードなど、ユーザーコンテンツに関連する画面が含まれます。

このサンプルコードでは、画面を2つのグループに分けています。認証済みユーザー向けのAppStackと、未認証ユーザー向けのAuthStackです。

import {createStackNavigator} from '@react-navigation/stack';
const Stack = createStackNavigator();

const AppStack = () => {
  return (
    <Stack.Navigator>
      <Stack.Screen name="Home Screen" component={HomeScreen} />
      <Stack.Screen name="Notification Screen" component={NotificationsScreen} />
      <Stack.Screen name="Profile Screen" component={ProfileScreen} />
      <Stack.Screen name="Setting Screen" component={SettingsScreen} />
    </Stack.Navigator>
  );
};

const AuthStack = () => {
  return (
    <Stack.Navigator>
      <Stack.Screen name="Sign In Screen" component={SignInScreen} />
      <Stack.Screen name="Register Screen" component={RegisterScreen} />
      <Stack.Screen name="Reset Password Screen" component={ResetPasScreen} />
    </Stack.Navigator>
  );
};

両方の画面グループを作成したら、ユーザーの認証状態に応じて表示するスタックを選択するRouterコンポーネントを作成します。

次に、両方のスタックをNavigationContainer内で使用します。react-navigationのコンポーネントが、React Nativeのすべてのルートナビゲーションをラップします。

import {NavigationContainer} from '@react-navigation/native';

import {AppStack} from './AppStack';
import {AuthStack} from './AuthStack';

export const Router = () => {
  return (
    <NavigationContainer>
      {authData ? <AppStack /> : <AuthStack />}
    </NavigationContainer>
  );
};

authDataは、認証画面(たとえばSignInScreen)でユーザーが入力するデータを表します。このコードでユーザーを認証します。

import React, {createContext, useState, useContext} from 'react';
import {AuthData, authService} from '../services/authService';

const AuthContext = createContext<AuthContextData>({} as AuthContextData);

const AuthProvider: React.FC = ({children}) => {
  const [authData, setAuthData] = useState<AuthData>();

  const [loading, setLoading] = useState(true);

  const signIn = async () => {
    const _authData = await authService.signIn(
      'test@email.com',
      '12345678',
    );

    setAuthData(_authData);
  };

  const signOut = async () => {
    setAuthData(undefined);
  };

  return (
    <AuthContext.Provider value={{authData, loading, signIn, signOut}}>
      {children}
    </AuthContext.Provider>
  );
};

signIn関数はAPIに接続し、レスポンスに基づいてauthDataを更新します。APIから取得したユーザープロフィールデータをここに保存できます。signOut関数はデータを消去します。場合によっては、APIに接続してトークンを無効化することもあります。

認証処理は非同期で、完了まで時間がかかるため、ローディング状態が必要です。この状態を監視し、サーバーがリクエストを処理している間、アプリのユーザーインターフェース(UI)に読み込みアニメーションを表示できます。

最後に、AuthContext.Providerでラップし、すべてのコンポーネントがContextにアクセスできるようにします。

OAuth 2.0プロトコル

React Nativeのモバイルアプリでは、ユーザー認証にOAuth 2.0プロトコルがよく使われます。OAuthは高度なセキュリティを備えているため、Webアプリやモバイルアプリで広く選ばれています。

OAuthフローに登場する4つの役割は次のとおりです。

  • リソースオーナー: リソースサーバー内のデータを所有します。たとえば、Facebookユーザーは自身のFacebookプロフィールデータのリソースオーナーです。

  • クライアントアプリ: データへのアクセスを求めるアプリケーション(例: Facebookのモバイルアプリ)

  • リソースサーバー: モバイルアプリケーションがアクセスするデータを保存するAPI(例: Facebook API)

  • 認可サーバー: OAuthの中心的なエンジン

OAuthでは、Google、Facebook、GitHubなどのサードパーティサービスを介してユーザーに認証を求めます。認証が完了すると、サードパーティサービスは検証コードとともにユーザーをアプリケーションへリダイレクトします。アプリケーションは、この検証コードをJSON Web Token(JWT)やその他のトークンと交換できます。

Webアプリでは、URLがWeb上で一意であることが保証されるため、こうしたリダイレクトは安全です。一方、モバイルアプリには、URLスキームを一元的に登録する方法がありません。

このセキュリティ上の懸念に対処するには、OAuth 2.0仕様の拡張機能であるPKCE(「ピクシー」と発音)によるチェックを追加する必要があります。PKCEはSHA 256暗号化ハッシュアルゴリズムを使い、認証リクエストとトークン交換リクエストが同じクライアントから送信されたことを検証します。

この方法により、認可フローを開始したアプリケーションだけが、検証コードをJWTと正常に交換できるようになります。

React NativeでネイティブOAuthを安全に実装する簡単な方法は、react-native-app-authのようなパッケージを使うことです。これは、OAuth 2.0およびOpenID Connectプロバイダーと通信するiOS向けAppAuth SDKとAndroid向けAppAuth SDKへのReact Nativeブリッジとして機能します。

このパッケージはPKCEに対応し、Google、Okta、GitHub、Coinbase、FusionAuthなど、さまざまなOpenIDおよびOAuth 2.0プロバイダーをサポートしています。

依存関係の脆弱性をスキャンする

開発者がReact Nativeだけで完全なネイティブアプリケーションを構築することはありません。課題を解決し、生産性を高めるために、サードパーティ製のツールをよく利用します。

アプリケーションには、完成度と機能性を高める複数のサードパーティ依存関係が含まれています。問題は、自分たちのコードが安全であっても、こうした依存関係によってアプリケーションに脆弱性が持ち込まれる可能性があることです。

Snykを使ってオープンソースパッケージの脆弱性をスキャンする方法はいくつかあります。人気のオープンソースReact NativeアートアプリEigenを例に見てみましょう。

まず、CLIツールをインストールし、Snykの無料アカウントに登録して、Snyk CLIでスキャンする方法があります。登録とインストールが完了したら、snyk authを実行してCLIをアカウントに接続します。

リポジトリを開発環境にクローンし、クローンしたプロジェクトのルートディレクトリでsnyk testを実行します。ルートにあるパッケージマニフェストをスキャンし、パッケージのバージョンに問題がないか確認します。

ヒント: snyk monitorも実行すると、初回スキャン後にプロジェクト内で検出された問題についても通知を受け取れます。

React Nativeプロジェクトの依存関係スキャン結果を示すターミナル出力。32件の問題と114件の脆弱なパスが報告されています。

この例では、react-nativeアプリのパッケージマニフェストから、いくつかの潜在的な問題が検出されました。検出された脆弱性を詳しく調べ、詳細情報を確認できるのも便利な点です。

一覧にあるHermes-engine 0.5.1には、クロスサイトスクリプティング(XSS)を引き起こす既知の脆弱性があります。この脆弱性に対処済みの0.7.2より新しいバージョンにアップグレードすれば、リスクを軽減できます。

hermes-engineのバージョン0.7.2未満に影響するクロスサイトスクリプティング(XSS)のセキュリティアドバイザリ。深刻度は中程度の6.1です。

React Nativeプロジェクトをスキャンするもう1つの方法は、Snykアカウントを利用するリポジトリに接続し、リポジトリを直接スキャンすることです。複数の貢献者がいるプロジェクトでも、コミットやマージのたびにチェックする継続的なスキャンで問題を検出できるのが利点です。

リポジトリを接続するには、Snykアカウントに登録します。次に、Snykダッシュボードでコードを保存しているリポジトリプラットフォームをクリックします。使用するプラットフォームがクイックメニューにない場合は、その他をクリックし、Snykのインテグレーションから検索します。

GitHub、Cloud Foundry、Bitbucket Cloud、CLI、公開GitHubリポジトリの監視、その他のオプションを表示するプロジェクト追加メニュー

接続手順に沿って進みます。リポジトリの接続ページで、スキャンするリポジトリを選択します。

「eigen」を検索し、developersteveの配下で「eigen」リポジトリを選択したSnykのGitHubリポジトリ選択画面。

選択したリポジトリを追加ボタンをクリックし、スキャンの完了を待ちます。スキャン対象と検出結果を確認できるのが便利です。

プロジェクトファイルのセキュリティ評価を一覧表示するコードセキュリティダッシュボード。カバレッジ、脆弱性、保守性、ライセンスのチェック項目を確認できます。

Snyk CLIによるスキャンと同様に、検出された脆弱性の詳細を確認し、リポジトリに自動修正を適用できます。

ansi-regexのセキュリティ脆弱性レポート。深刻度の高い正規表現サービス拒否(ReDoS)脆弱性と、影響を受けるバージョンを示しています。

アプリの開発中に依存関係を自動でスキャンして脆弱性を検出できるよう、好みのコードエディターにSnykを統合することもできます。たとえば、Visual Studio Code用Snyk拡張機能は、パッケージマニフェストだけでなく、そのパッケージがコード内で呼び出されている箇所も含めて、潜在的な脆弱性を特定します。

まとめ

モバイルアプリで扱うデータが高度化するにつれ、ユーザー情報を適切に保護し、エンドユーザーを守ることがこれまで以上に重要になっています。Webアプリケーションと同様に、React Nativeアプリも、安全なデータ転送、認証プロトコル、依存関係の脆弱性など、同じ種類のセキュリティ課題に直面します。

Reactアプリケーションのセキュリティ問題を見つけて修正するために、チームで活用できるReactのセキュリティに関するベストプラクティスのチェックリストをご覧ください。

CTFを始めよう

オンデマンドのバーチャル入門ワークショップで、CTFチャレンジの解き方を学びましょう。

Kingsley Ubah は、経験豊富なフルスタックWeb開発者であり、テクニカルライターです。

続きを読む

blog feature ai lilac
Blog

エージェント型修正で脆弱性バックログをプルリクエストに変える

Evo Remediation Agentがオープンプレビューで利用可能になり、Evo Agentic AppSecに依存関係の自動解決機能が加わりました。AppSecチームは継続的な修正キャンペーンを実施できるようになりました

AI Background min crop
Blog

AIが、あなたが受け入れてきたあらゆるリスクを一気に顕在化させる:AI Security Panel: Unfilteredから得た7つの洞察

AIによって攻撃はより安価かつ高速になり、組織がすでに抱えているリスクが露呈しています。4人のセキュリティリーダーが、機械の速度に対応する防御と迅速な修復が今不可欠である理由を語ります。

blog feature ai violet
Blog

VulnBump:AIエージェントはビルドを壊さずに脆弱な依存関係を修正できるか?

VulnBumpでは、実際の依存関係のアップグレード307件をAIエージェントで検証します。脆弱性が解消され、ビルドとテストが正常に通った場合にのみ、修正成功と判定します。