メインコンテンツへスキップ

セキュリティ

アプリを構築する際、セキュリティは見落とされがちです。完全に侵入不可能なソフトウェアを構築することは不可能であることは事実です。私たちはまだ完全に侵入不可能な錠前を発明していません(結局のところ、銀行の金庫は今でも破られます)。しかし、悪意のある攻撃の被害に遭ったり、セキュリティ脆弱性に晒されたりする確率は、そのような事態からアプリケーションを保護するために費やす努力に反比例します。普通の南京錠はピッキング可能ですが、それでもキャビネットフックよりははるかに突破が困難です!

このガイドでは、機密情報の保存、認証、ネットワークセキュリティに関するベストプラクティスと、アプリのセキュリティを確保するのに役立つツールについて学びます。これは飛行前のチェックリストではなく、選択肢のカタログであり、それぞれがアプリとユーザーをさらに保護するのに役立ちます。

機密情報の保存

機密性の高いAPIキーをアプリのコード内に決して保存しないでください。コードに含まれるものはすべて、アプリのバンドルを調査する誰でも平文でアクセスできます。react-native-dotenvreact-native-configのようなツールは、APIエンドポイントのような環境固有の変数を追加するのに最適ですが、これらはしばしば秘密情報やAPIキーを含むことがあるサーバー側の環境変数と混同してはいけません。

アプリから何らかのリソースにアクセスするためにAPIキーや秘密情報が必要な場合、最も安全な方法は、アプリとリソースの間にオーケストレーションレイヤーを構築することです。これは、必要なAPIキーや秘密情報を付けてリクエストを転送できるサーバーレス関数(例:AWS LambdaやGoogle Cloud Functionsを使用)である可能性があります。サーバーサイドコード内の秘密情報は、アプリコード内の秘密情報のようにAPIコンシューマーからアクセスすることはできません。

永続化されたユーザーデータについては、その機密性に基づいて適切な種類のストレージを選択してください。アプリが使用されるにつれて、オフラインでの使用をサポートしたり、ネットワークリクエストを削減したり、ユーザーがアプリを使用するたびに再認証する必要がないようにセッション間でアクセストークンを保存したりするために、デバイスにデータを保存する必要性がしばしば生じます。

永続化 vs 非永続化 — 永続化データはデバイスのディスクに書き込まれ、これによりアプリは起動をまたいでデータを読み取ることができます。ネットワークリクエストを再度行ったり、ユーザーに再入力を求めたりする必要がありません。しかし、これは同時に、そのデータが攻撃者によってアクセスされやすくなる可能性も高めます。非永続化データはディスクに書き込まれることはないため、アクセスするデータもありません!

Async Storage

Async Storageは、React Native用のコミュニティによってメンテナンスされているモジュールで、非同期で暗号化されていないキーバリューストアを提供します。Async Storageはアプリ間で共有されません。各アプリは独自のサンドボックス環境を持ち、他のアプリのデータにアクセスすることはできません。

次の場合にAsync Storageを使用してください...次の場合にAsync Storageを使用しないでください...
機密性の低いデータをアプリの実行をまたいで永続化する場合トークンの保存
Reduxのstateを永続化する場合秘密情報
GraphQLのstateを永続化する場合
アプリ全体で共通のグローバル変数を保存する場合

開発者向けメモ

Async Storageは、WebにおけるLocal StorageのReact Native版です。

セキュアストレージ

React Nativeには、機密データを保存する方法がバンドルされていません。しかし、AndroidおよびiOSプラットフォームには既存のソリューションがあります。

iOS - Keychain Services

Keychain Servicesを使用すると、ユーザーの機密情報を小さな塊で安全に保存できます。これは、証明書、トークン、パスワード、およびAsync Storageに入れるべきではないその他の機密情報を保存するのに理想的な場所です。

Android - Secure Shared Preferences

Shared Preferencesは、永続的なキーバリューデータストアのAndroid版です。Shared Preferencesのデータはデフォルトでは暗号化されていませんが、Encrypted Shared PreferencesはAndroid用のShared Preferencesクラスをラップし、キーと値を自動的に暗号化します。

Android - Keystore

Android Keystoreシステムを使用すると、暗号鍵をコンテナに保存して、デバイスからの抽出をより困難にすることができます。

iOSのKeychainサービスやAndroidのSecure Shared Preferencesを使用するには、自分でブリッジを作成するか、それらをラップして統一されたAPIを提供するライブラリを自己責任で使用することができます。検討すべきライブラリには以下のようなものがあります。

意図せずに機密情報を保存したり公開したりしないように注意してください。これは、例えば、機密性の高いフォームデータをReduxのstateに保存し、stateツリー全体をAsync Storageに永続化したり、SentryやCrashlyticsなどのアプリケーション監視サービスにユーザートークンや個人情報を送信したりすることで、誤って発生する可能性があります。

認証とディープリンク

モバイルアプリには、Webには存在しない特有の脆弱性があります。それはディープリンクです。ディープリンクは、外部ソースからネイティブアプリケーションに直接データを送信する方法です。ディープリンクはapp://のような形式で、appはアプリのスキームであり、//に続くものはリクエストを処理するために内部で使用されます。

例えば、eコマースアプリを構築している場合、app://products/1を使用してアプリにディープリンクし、IDが1の商品の詳細ページを開くことができます。これらはWeb上のURLのようなものと考えることができますが、1つの決定的な違いがあります。

ディープリンクは安全ではなく、機密情報を決して送信してはいけません。

ディープリンクが安全でない理由は、URLスキームを登録する中央集権的な方法がないためです。アプリケーション開発者として、iOSではXcodeで設定するか、Androidではインテントを追加することで、ほとんど任意のURLスキームを使用できます。

悪意のあるアプリケーションが、同じスキームに登録することでディープリンクをハイジャックし、リンクに含まれるデータへのアクセス権を取得することを妨げるものは何もありません。app://products/1のようなものを送信することは有害ではありませんが、トークンを送信することはセキュリティ上の懸念事項です。

オペレーティングシステムがリンクを開くために2つ以上のアプリケーションから選択する必要がある場合、Androidはユーザーに曖昧性解消ダイアログを表示し、リンクを開くために使用するアプリケーションを選択するよう求めます。しかし、iOSではオペレーティングシステムが選択を行うため、ユーザーは全く気づきません。Appleは後のiOSバージョン(iOS 11)でこの問題に対処するための措置を講じ、先着順の原則を導入しましたが、この脆弱性はまださまざまな方法で悪用される可能性があります。詳細はこちらで読むことができます。iOSでアプリ内のコンテンツに安全にリンクするには、ユニバーサルリンクを使用します。

OAuth2とリダイレクト

OAuth2認証プロトコルは今日非常に人気があり、最も完全で安全なプロトコルとして評価されています。OpenID Connectプロトコルもこれに基づいています。OAuth2では、ユーザーは第三者を介して認証するよう求められます。正常に完了すると、この第三者は検証コードとともにリクエスト元のアプリケーションにリダイレクトします。この検証コードはJWT(JSON Web Token)と交換できます。JWTは、Web上で当事者間で情報を安全に送信するためのオープンスタンダードです。

Web上では、このリダイレクトステップは安全です。なぜならWeb上のURLは一意であることが保証されているからです。これはアプリには当てはまりません。なぜなら、前述のようにURLスキームを登録する中央集権的な方法がないからです!このセキュリティ上の懸念に対処するためには、PKCEという形で追加のチェックを追加する必要があります。

「ピクシー」と発音されるPKCEは、Proof of Key Code Exchangeの略で、OAuth 2仕様の拡張です。これは、認証リクエストとトークン交換リクエストが同じクライアントから来ていることを検証する追加のセキュリティ層を追加することを含みます。PKCEはSHA 256暗号学的ハッシュアルゴリズムを使用します。SHA 256は、任意のサイズのテキストやファイルに対して一意の「署名」を作成しますが、それは以下の特徴を持ちます。

  • 入力ファイルに関係なく常に同じ長さ
  • 同じ入力に対して常に同じ結果を生成することが保証されている
  • 一方向性(つまり、元の入力を明らかにするためにリバースエンジニアリングすることはできない)

これで2つの値ができます

  • code_verifier - クライアントによって生成された大きなランダムな文字列
  • code_challenge - code_verifierのSHA 256

最初の/authorizeリクエスト中に、クライアントはメモリに保持しているcode_verifierに対応するcode_challengeも送信します。authorizeリクエストが正しく返された後、クライアントはcode_challengeを生成するために使用されたcode_verifierも送信します。するとIDPはcode_challengeを計算し、最初の/authorizeリクエストで設定されたものと一致するかどうかを確認し、値が一致する場合にのみアクセストークンを送信します。

これにより、最初の認証フローをトリガーしたアプリケーションのみが、検証コードをJWTと正常に交換できることが保証されます。したがって、悪意のあるアプリケーションが検証コードにアクセスできたとしても、それだけでは役に立ちません。これが実際にどのように機能するかを確認するには、この例を参照してください。

ネイティブOAuthで検討すべきライブラリはreact-native-app-authです。React-native-app-authは、OAuth2プロバイダーと通信するためのSDKです。ネイティブのAppAuth-iOSおよびAppAuth-Androidライブラリをラップし、PKCEをサポートできます。

react-native-app-authは、IDプロバイダーがサポートしている場合にのみPKCEをサポートできます。

OAuth2 with PKCE

ネットワークセキュリティ

APIは常にSSL暗号化を使用すべきです。SSL暗号化は、リクエストされたデータがサーバーから離れてクライアントに到達するまでの間に平文で読み取られるのを防ぎます。エンドポイントがhttp://ではなくhttps://で始まることで、そのエンドポイントが安全であることがわかります。

SSL Pinning

httpsエンドポイントを使用しても、データが傍受される脆弱性が残る可能性があります。httpsでは、クライアントはサーバーがクライアントにプリインストールされている信頼された認証局によって署名された有効な証明書を提供できる場合にのみサーバーを信頼します。攻撃者は、ユーザーのデバイスに悪意のあるルートCA証明書をインストールすることでこれを悪用し、クライアントが攻撃者によって署名されたすべての証明書を信頼するように仕向けることができます。したがって、証明書だけに頼っていると、中間者攻撃に対して脆弱なままになる可能性があります。

SSL Pinning (SSL証明書ピンニング)は、この攻撃を回避するためにクライアント側で使用できる手法です。これは、開発中に信頼できる証明書のリストをクライアントに埋め込む(またはピン留めする)ことで機能し、信頼できる証明書のいずれかで署名されたリクエストのみが受け入れられ、自己署名証明書は受け入れられなくなります。

SSLピンニングを使用する場合、証明書の有効期限に注意する必要があります。証明書は1~2年ごとに期限切れになり、期限が切れると、サーバーだけでなくアプリ内でも更新する必要があります。サーバー上の証明書が更新されるとすぐに、古い証明書が埋め込まれたアプリは動作しなくなります。

まとめ

セキュリティを扱う上で絶対確実な方法はありませんが、意識的な努力と注意を払うことで、アプリケーションでのセキュリティ侵害の可能性を大幅に減らすことができます。アプリケーションに保存されているデータの機密性、ユーザー数、ハッカーがアカウントにアクセスした場合に与える可能性のある損害に応じて、セキュリティに投資してください。そして覚えておいてください:そもそもリクエストされなかった情報にアクセスすることは、はるかに困難です。