安全なクリップボードの処理

OWASP カテゴリ: MASVS-CODE: コード品質

概要

Android には、アプリケーション間でデータをコピー&ペーストするための強力なフレームワーク(クリップボード)が用意されています。この機能が適切に実装されていない場合、ユーザー関連データが不正な悪意のある行為者やアプリケーションに公開される可能性があります。

クリップボード データの公開に関連する具体的なリスクは、アプリの性質と、アプリが処理する個人情報(PII)によって異なります。特に、支払いデータを公開する可能性のある金融アプリや、2 要素認証(2FA)コードを処理するアプリでは、影響が大きくなります。

クリップボードのデータを漏洩するために利用される可能性のある攻撃ベクトルは、Android のバージョンによって異なります。

  • Android 10(API レベル 29)より前の Android バージョンでは、バックグラウンド アプリケーションがフォアグラウンド アプリのクリップボード情報にアクセスできるため、悪意のあるユーザーがコピーされたデータに直接アクセスできる可能性があります。
  • Android 12(API レベル 31)以降では、アプリがクリップボード内のデータにアクセスして貼り付けるたびに、トースト メッセージがユーザーに表示されるため、攻撃が気づかれにくくなるのを防ぐことができます。また、PII を保護するために、Android は ClipDescription.EXTRA_IS_SENSITIVE または android.content.extra.IS_SENSITIVE の特別なフラグをサポートしています。これにより、デベロッパーはキーボード GUI 内のクリップボード コンテンツのプレビューを視覚的に難読化し、コピーされたデータが平文で視覚的に表示されて悪意のあるアプリに盗まれる可能性を回避できます。前述のフラグのいずれかを実装しないと、攻撃者がショルダー サーフィンや、バックグラウンドで実行中に正当なユーザーのアクティビティのスクリーンショットを撮ったり動画を録画したりする悪意のあるアプリを介して、クリップボードにコピーされたセンシティブ データを引き出す可能性があります。

影響

クリップボードの不適切な処理を悪用されると、ユーザー関連の機密データや財務データが悪意のあるアクターによって漏洩する可能性があります。これにより、攻撃者はフィッシング キャンペーンや個人情報の盗難などのさらなる攻撃を実行しやすくなります。

リスクの軽減

機密データを報告する

このソリューションは、キーボード GUI 内のクリップボード コンテンツのプレビューを視覚的に難読化するために使用されます。パスワードやクレジット カード データなど、コピー可能な機密データは、ClipboardManager.setPrimaryClip() を呼び出す前に ClipDescription.EXTRA_IS_SENSITIVE または android.content.extra.IS_SENSITIVE でフラグを設定する必要があります。

Kotlin

// If your app is compiled with the API level 33 SDK or higher.
clipData.apply {
    description.extras = PersistableBundle().apply {
        putBoolean(ClipDescription.EXTRA_IS_SENSITIVE, true)
    }
}

// If your app is compiled with API level 32 SDK or lower.
clipData.apply {
    description.extras = PersistableBundle().apply {
        putBoolean("android.content.extra.IS_SENSITIVE", true)
    }
}

Java

// If your app is compiled with the API level 33 SDK or higher.
PersistableBundle extras = new PersistableBundle();
extras.putBoolean(ClipDescription.EXTRA_IS_SENSITIVE, true);
clipData.getDescription().setExtras(extras);

// If your app is compiled with API level 32 SDK or lower.
PersistableBundle extras = new PersistableBundle();
extras.putBoolean("android.content.extra.IS_SENSITIVE", true);
clipData.getDescription().setExtras(extras);

最新バージョンの Android を適用する

アプリが Android 10(API 29)以降の Android バージョンで実行されるように強制することで、バックグラウンド プロセスがフォアグラウンド アプリケーションのクリップボード データにアクセスすることを防ぎます。

アプリが Android 10(API 29)以降でのみ実行されるようにするには、Android Studio のプロジェクト内の Gradle ビルドファイルで、バージョン設定に次の値を設定します。

Groovy

android {
      namespace 'com.example.testapp'
      compileSdk [SDK_LATEST_VERSION]

      defaultConfig {
          applicationId "com.example.testapp"
          minSdk 29
          targetSdk [SDK_LATEST_VERSION]
          versionCode 1
          versionName "1.0"
          ...
      }
      ...
    }
    ...

Kotlin

android {
      namespace = "com.example.testapp"
      compileSdk = [SDK_LATEST_VERSION]

      defaultConfig {
          applicationId = "com.example.testapp"
          minSdk = 29
          targetSdk = [SDK_LATEST_VERSION]
          versionCode = 1
          versionName = "1.0"
          ...
      }
      ...
    }
    ...

定義された期間の経過後にクリップボードの内容を削除する

Android 10(API レベル 29)より前の Android バージョンで実行されるアプリの場合、バックグラウンド アプリはクリップボード データにアクセスできます。このリスクを軽減するため、特定の期間が経過した後にクリップボードにコピーされたデータをクリアする関数を実装すると便利です。この関数は、Android 13(API レベル 33)以降で自動的に実行されます。以前のバージョンの Android では、アプリのコードに次のスニペットを含めることで、この削除を実行できます。

Kotlin

//The Executor makes this task Asynchronous so that the UI continues being responsive
backgroundExecutor.schedule({
    //Creates a clip object with the content of the Clipboard
    val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
    val clip = clipboard.primaryClip
    //If SDK version is higher or equal to 28, it deletes Clipboard data with clearPrimaryClip()
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
        clipboard.clearPrimaryClip()
    } else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
    //If SDK version is lower than 28, it will replace Clipboard content with an empty value
        val newEmptyClip = ClipData.newPlainText("EmptyClipContent", "")
        clipboard.setPrimaryClip(newEmptyClip)
     }
//The delay after which the Clipboard is cleared, measured in seconds
}, 5, TimeUnit.SECONDS)

Java

//The Executor makes this task Asynchronous so that the UI continues being responsive

ScheduledExecutorService backgroundExecutor = Executors.newSingleThreadScheduledExecutor();

backgroundExecutor.schedule(new Runnable() {
    @Override
    public void run() {
        //Creates a clip object with the content of the Clipboard
        ClipboardManager clipboard = (ClipboardManager)getSystemService(Context.CLIPBOARD_SERVICE);
        ClipData clip = clipboard.getPrimaryClip();
        //If SDK version is higher or equal to 28, it deletes Clipboard data with clearPrimaryClip()
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
            clipboard.clearPrimaryClip();
            //If SDK version is lower than 28, it will replace Clipboard content with an empty value
        } else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
            ClipData newEmptyClip = ClipData.newPlainText("EmptyClipContent", "");
            clipboard.setPrimaryClip(newEmptyClip);
        }
    //The delay after which the Clipboard is cleared, measured in seconds
    }, 5, TimeUnit.SECONDS);

リソース