OWASP 類別:MASVS-CODE:程式碼品質
總覽
Android 提供強大的架構,稱為剪貼簿,可在應用程式之間複製及貼上資料。如果這項功能導入方式不正確,可能會將使用者相關資料洩漏給未經授權的惡意人士或應用程式。
與剪貼簿資料外洩相關的具體風險取決於應用程式的性質,以及該應用程式處理的個人識別資訊 (PII)。對於金融應用程式來說,影響特別嚴重,因為這類應用程式可能會洩漏付款資料,或處理雙重驗證 (2FA) 碼。
可用來竊取剪貼簿資料的攻擊媒介會因 Android 版本而異:
- 舊版 Android (低於 Android 10,API 級別 29) 允許背景應用程式存取前景應用程式的剪貼簿資訊,因此惡意人士可能會直接存取任何複製的資料。
- 從 Android 12 (API 級別 31) 起,每當應用程式存取剪貼簿中的資料並貼上時,系統就會向使用者顯示 Toast 訊息,讓攻擊行為更難以隱匿。此外,為了保護個人識別資訊,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 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);