OWASP 類別:MASVS-CODE:程式碼品質
總覽
Android 提供強大的剪貼簿架構,可在應用程式之間複製及貼上資料。如果這項功能實作不當,可能會導致未經授權的惡意人士或應用程式取得使用者相關資料。
剪貼簿資料外洩的具體風險取決於應用程式的性質,以及應用程式處理的個人識別資訊 (PII)。這類問題對財務應用程式的影響尤其嚴重,因為這類應用程式可能會洩漏付款資料,或是處理雙重驗證 (2FA) 碼。
可竊取剪貼簿資料的攻擊媒介會因 Android 版本而異:
- Android 10 (API 級別 29) 之前的版本允許背景應用程式存取前景應用程式的剪貼簿資訊,惡意人士可能藉此直接存取任何複製的資料。
- 從 Android 12 (API 級別 31) 開始,應用程式每次存取剪貼簿中的資料並貼上時,系統都會向使用者顯示浮動式訊息,讓攻擊行為更難以不被察覺。此外,為保護個人識別資訊,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);