ログ情報漏洩

OWASP カテゴリ: MASVS-STORAGE: ストレージ

概要

ログ情報漏洩とは、アプリによって機密データがデバイスのログに出力される脆弱性のことです。こうした機密データは、悪意のある行為者に漏洩した場合、ユーザーの認証情報、個人情報(PII)などの非常に価値の高い情報につながる可能性があり、さらなる攻撃を招く恐れがあります。

この問題は、次のいずれかのシナリオで発生する可能性があります。

  • アプリで生成されたログ:
    • 未承認の行為者がログにアクセスすることを意図的に許可していたが、ログに機密データが誤って含まれていた場合。
    • ログに意図的に機密データを含めていたが、誤りにより未承認の行為者がログにアクセスできるようになっていた場合。
    • 一般的なエラーログに機密データが出力される場合(トリガーされたエラー メッセージによっては、機密データが出力されることがある)。
  • 外部で生成されたログ:
    • 外部コンポーネントが機密データを含むログの出力を担っている場合。

Android の Log.* ステートメントは、一般的なメモリバッファである logcat に書き込みを行います。Android 4.1(API レベル 16)以降では、READ_LOGS 権限を宣言して、特権システムアプリのみに logcat の読み取り権限を付与できます。しかし、Android は非常に多様なデバイスをサポートしており、一部のデバイスではプリインストールされたアプリが READ_LOGS 権限を宣言することがあります。結果的に、logcat に直接ロギングすることは、データ漏洩が発生しやすいため推奨されません。

logcat へのすべてのロギングが、アプリの非デバッグ版でサニタイズされるようにしてください。機密性が高い可能性があるすべてのデータを削除します。また、追加の予防措置として、R8 などのツールを使用して、警告とエラーを除くすべてのログレベルを削除します。より詳細なログが必要な場合は、システムログを使用する代わりに、内部ストレージを使用して独自のログを直接管理します。

影響

ログ情報漏洩の脆弱性クラスの重大度は、コンテキストと機密データの種類によって異なります。全体として、この脆弱性クラスは、重要な可能性がある情報(PII や認証情報など)の機密性の喪失という形で影響を及ぼします。

リスクの軽減

全般

設計と実装時の一般的な予防策として、最小権限の原則に沿って信頼境界を設定します。機密データが信頼境界を越えたり、信頼領域の外部に到達したりしないようにすることが理想的です。これにより、権限の分離が強化されます。

機密データはログに記録しないでください。可能な限り、コンパイル時定数のみをログに記録します。コンパイル時定数のアノテーションには、ErrorProne ツールを使用できます。

トリガーされたエラーによっては予期しない情報(機密データなど)が含まれるステートメントを出力するログがありますが、そうしたログは使用しないでください。ログとエラーログに出力されるデータには、可能な限り予測可能な情報のみを含める必要があります。

logcat にはロギングしないでください。これは、READ_LOGS 権限を持つアプリが原因で、logcat へのロギングがプライバシーの問題になる可能性があるためです。また、アラートをトリガーすることもクエリを実行することもできないため、有用性に欠けます。そのため、アプリのデベロッパー ビルドにのみ logcat バックエンドを設定することをおすすめします。

ほとんどのログ管理ライブラリでは、ログレベルを定義できます。これにより、デバッグログと本番環境ログの間で異なる量の情報をロギングできます。製品テストの終了後、すぐにログレベルを「debug」以外に変更してください。

本番環境から可能な限り多くのログレベルを削除します。本番環境でログを保持せざるを得ない場合は、ログ ステートメントから非定数変数を削除してください。次のシナリオが考えられます。

  • 本番環境からすべてのログを削除できる。
  • 本番環境で警告ログとエラーログを保持する必要がある。

どちらの場合も、R8 などのライブラリを使用してログを自動的に削除します。手動でログを削除しようとすると、エラーが発生しやすくなります。コードの最適化の一環として R8 を設定することで、デバッグでは保持するものの本番環境では削除するログレベルを安全に削除できます。

本番環境にログインする場合には、インシデント発生時に条件付きでロギングをシャットダウンするために使用できるフラグを準備します。インシデント対応フラグでは、デプロイの安全性、デプロイのスピードと容易さ、ログ秘匿化の綿密さ、メモリ使用量、すべてのログメッセージをスキャンする際のパフォーマンス コストを優先する必要があります。

R8 を使用して、logcat へのロギングを本番環境ビルドから削除する

Android Studio 3.4 または Android Gradle プラグイン 3.4.0 以降の場合、コードの最適化と圧縮を行うためのデフォルトのコンパイラは R8 になります。ただし、R8 を有効にする必要があります。

ProGuard は R8 に置き換えられましたが、プロジェクトのルートフォルダにあるルールファイルの名前は、引き続き proguard-rules.pro となっています。次のスニペットは、本番環境から警告とエラーを除くすべてのログを削除する proguard-rules.pro ファイルの例を示しています。

-assumenosideeffects class android.util.Log {
    private static final String TAG = "MyTAG";
    public static boolean isLoggable(java.lang.String, int);
    public static int v(TAG, "My log as verbose");
    public static int d(TAG, "My log as debug");
    public static int i(TAG, "My log as information");
}

次の proguard-rules.pro ファイルの例では、本番環境からすべてのログを削除します。

-assumenosideeffects class android.util.Log {
    private static final String TAG = "MyTAG";
    public static boolean isLoggable(java.lang.String, int);
    public static int v(TAG, "My log as verbose");
    public static int d(TAG, "My log as debug");
    public static int i(TAG, "My log as information");
    public static int w(TAG, "My log as warning");
    public static int e(TAG, "My log as error");
}

R8 には、アプリ圧縮機能とログ削除機能が用意されています。ログ削除機能のためにのみ R8 を使用する場合は、proguard-rules.pro ファイルに以下を追加します。

-dontwarn **
-dontusemixedcaseclassnames
-dontskipnonpubliclibraryclasses
-dontpreverify
-verbose

-optimizations !code/simplification/arithmetic,!code/allocation/variable
-keep class **
-keepclassmembers class *{*;}
-keepattributes *

本番環境で機密データを含む最終的なログをサニタイズする

機密データの漏洩を避けるため、logcat へのすべてのロギングが、アプリの非デバッグ版でサニタイズされるようにします。機密性が高い可能性があるすべてのデータを削除します。

例:

Kotlin

data class Credential<T>(val data: String) {
  /** Returns a redacted value to avoid accidental inclusion in logs. */
  override fun toString() = "Credential XX"
}

fun checkNoMatches(list: List<Any>) {
    if (!list.isEmpty()) {
          Log.e(TAG, "Expected empty list, but was %s", list)
    }
}

Java

public class Credential<T> {
  private T t;
  /** Returns a redacted value to avoid accidental inclusion in logs. */
  public String toString(){
         return "Credential XX";
  }
}

private void checkNoMatches(List<E> list) {
   if (!list.isEmpty()) {
          Log.e(TAG, "Expected empty list, but was %s", list);
   }
}

ログ内の機密データを秘匿化する

ログに機密データを含める必要がある場合は、ログを出力する前にサニタイズし、機密データを削除または難読化することをおすすめします。これは次のいずれかの方法で行います。

  • トークン化。トークンを使用してシークレットを参照できる暗号化管理システムなどに機密データが保存されている場合は、機密データの代わりにトークンをログに記録します。
  • データ マスキング。データ マスキングは一方向で行うため、元に戻せません。この方法では、機密データの元のバージョンと構造的に類似しているものの、フィールド内に含まれる最も機密性の高い情報は非表示になっているバージョンを作成します(例: クレジット カード番号 1234-5678-9012-3456XXXX-XXXX-XXXX-1313 に置き換えます)。アプリを本番環境にリリースする前に、データ マスキングの使用を精査するセキュリティ審査プロセスを完了することをおすすめします。警告: 機密データの一部を公開しただけでセキュリティに大きな影響を与える可能性がある場合(パスワードを取り扱う場合など)は、データ マスキングを使用しないでください。
  • 秘匿化。秘匿化はマスキングと似ていますが、フィールドに含まれるすべての情報を非表示にします(例: クレジット カード番号 1234-5678-9012-3456XXXX-XXXX-XXXX-XXXX に置き換えます)。
  • フィルタ。選択したロギング ライブラリにフォーマット文字列が存在しない場合は実装し、ログ ステートメント内の非定数値を容易に変更できるようにします。

次のコード スニペットに示すように、ログ出力は、出力前にすべてのログがサニタイズされるように、「logs sanitizer」コンポーネントを介してのみ実行する必要があります。

Kotlin

data class ToMask<T>(private val data: T) {
  // Prevents accidental logging when an error is encountered.
  override fun toString() = "XX"

  // Makes it more difficult for developers to invoke sensitive data
  // and facilitates sensitive data usage tracking.
  fun getDataToMask(): T = data
}

data class Person(
  val email: ToMask<String>,
  val username: String
)

fun main() {
    val person = Person(
        ToMask("name@gmail.com"),
        "myname"
    )
    println(person)
    println(person.email.getDataToMask())
}

Java

public class ToMask<T> {
  // Prevents accidental logging when an error is encountered.
  public String toString(){
         return "XX";
  }

  // Makes it more difficult for developers to invoke sensitive data
  // and facilitates sensitive data usage tracking.
  public T  getDataToMask() {
    return this;
  }
}

public class Person {
  private ToMask<String> email;
  private String username;

  public Person(ToMask<String> email, String username) {
    this.email = email;
    this.username = username;
  }
}

public static void main(String[] args) {
    Person person = new Person(
        ToMask("name@gmail.com"),
        "myname"
    );
    System.out.println(person);
    System.out.println(person.email.getDataToMask());
}