安全剪貼簿處理

OWASP 類別:MASVS-CODE:程式碼品質

總覽

Android 提供強大的剪貼簿架構,可在應用程式之間複製及貼上資料。如果這項功能實作不當,可能會導致未經授權的惡意人士或應用程式取得使用者相關資料。

剪貼簿資料外洩的具體風險取決於應用程式的性質,以及應用程式處理的個人識別資訊 (PII)。這類問題對財務應用程式的影響尤其嚴重,因為這類應用程式可能會洩漏付款資料,或是處理雙重驗證 (2FA) 碼。

可竊取剪貼簿資料的攻擊媒介會因 Android 版本而異:

  • Android 10 (API 級別 29) 之前的版本允許背景應用程式存取前景應用程式的剪貼簿資訊,惡意人士可能藉此直接存取任何複製的資料。
  • 從 Android 12 (API 級別 31) 開始,應用程式每次存取剪貼簿中的資料並貼上時,系統都會向使用者顯示浮動式訊息,讓攻擊行為更難以不被察覺。此外,為保護個人識別資訊,Android 支援 ClipDescription.EXTRA_IS_SENSITIVEandroid.content.extra.IS_SENSITIVE 特殊旗標。開發人員可藉此在鍵盤 GUI 中,以視覺化方式遮蓋剪貼簿內容預覽畫面,避免複製的資料以明文顯示,並防止惡意應用程式竊取資料。如果未實作上述任一標記,攻擊者可能會透過肩窺或惡意應用程式,竊取複製到剪貼簿的機密資料。這些惡意應用程式會在背景執行,並擷取螢幕截圖或錄製影片,記錄正當使用者的活動。

影響

如果剪貼簿處理方式不當,惡意人士可能會竊取使用者的敏感或金融資料。這可能會協助攻擊者採取進一步行動,例如發起網路釣魚活動或竊取身分。

因應措施

標記機密資料

這項解決方案用於在鍵盤 GUI 中,以視覺化方式遮蓋剪貼簿內容預覽畫面。任何可複製的私密資料 (例如密碼或信用卡資料) 都應在呼叫 ClipboardManager.setPrimaryClip() 前,先以 ClipDescription.EXTRA_IS_SENSITIVEandroid.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 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 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);

資源