OWASP 類別:MASVS-STORAGE:儲存空間
簡介
「記錄資訊外洩」是一種安全漏洞,會使應用程式將機密資料輸出至裝置記錄。如果這些資料洩漏給惡意人士,可能會是具備高價值的機密資訊 (例如使用者的憑證或個人識別資訊 (PII),也可能會遭到進一步攻擊。
下列任一情況都可能發生這個問題:
- 應用程式產生的記錄:
- 開發人員有意允許未經授權的人士存取記錄檔,但不小心在記錄檔中納入了機密資料。
- 開發人員有意在記錄檔中納入機密資料,但不小心讓未經授權的人士存取記錄檔。
- 視實際觸發的錯誤訊息而定,有時一般錯誤記錄可能會輸出機密資料。
- 外部產生的記錄:
- 外部元件負責輸出含有機密資料的記錄。
Android Log.*
陳述式會寫入通用記憶體緩衝區 logcat
。從 Android 4.1 (API 級別 16) 開始,只有特殊權限系統應用程式才能取得讀取 logcat
的權限,方法是宣告 READ_LOGS
權限。不過,Android 支援的裝置種類繁多,有時某些裝置預先載入的應用程式會宣告 READ_LOGS
權限。因此,我們不建議直接記錄到 logcat
,因為這樣更容易造成資料外洩。
請確保 logcat
的所有記錄均已在應用程式的非偵錯版本中經過處理。移除任何可能含有機密內容的資料。為求謹慎,除了警示和錯誤除外,請使用 R8 等工具移除所有其他的記錄層級。如果您需要更詳細的記錄,請使用內部儲存空間並直接自行管理記錄,不要使用系統記錄。
影響
根據機密資料的內容與類型而定,記錄資訊外洩安全漏洞類別的嚴重性會有所差異。整體而言,這個安全漏洞類別會導致潛在重要機密資訊 (例如 PII 和憑證) 失去機密性。
因應措施
一般做法
一般的預防措施是在設計及實作階段,依據最低權限原則繪製信任界線。理想情況下,機密資料不應跨越或觸及任何信任區域以外的範圍。這麼做可強化權限分離機制。
不要記錄機密資料。請盡可能只記錄編譯時間常數。您可以使用 ErrorProne 工具進行編譯時間常數註解。
避免記錄輸出可能含有非預期資訊的內容,包括機密資料,具體內容視觸發的錯誤而定。記錄和錯誤記錄中輸出的資料,應該盡可能只包括可預測的資訊。
避免記錄到 logcat
。這是因為如果應用程式具備 READ_LOGS
權限,記錄到 logcat
可能會發生隱私權問題。此外,由於這麼做無法觸發快訊或進行查詢,因此效果不佳。建議應用程式只針對開發人員版本設定 logcat
後端。
大部分的記錄管理程式庫允許定義記錄層級,以記錄偵錯階段和實際工作環境階段間的不同資訊量。請在產品測試結束後變更記錄層級,以便與「偵錯」階段的記錄區分。
請盡可能從實際工作環境中移除記錄層級。如果您無法避免將記錄保留在實際工作環境中,請從記錄陳述式中移除非常數性質的變數。以下是可能發生的情況:
- 您可以移除實際工作環境中的所有記錄。
- 您必須在實際工作環境中保留警告和錯誤記錄。
無論是哪一種情況,請使用 R8 等程式庫自動移除記錄。嘗試手動移除記錄很容易出錯。進行程式碼最佳化時,可以對 R8 進行設定,安全地移除想在偵錯階段中保留、但在實際工作環境中移除的記錄層級。
如果您要在實際工作環境中啟用記錄檔,請先準備用來關閉記錄功能的旗標,以便在特定狀況下停用記錄程序。事件回應旗標應優先考量以下項目:部署的安全性、部署的速度和易用性、遮蓋記錄的完善程度、記憶體用量,以及掃描每個記錄訊息的效能成本。
使用 R8 將 Logcat 記錄從實際工作環境中去除。
在 Android Studio 3.4 或 Android Gradle 外掛程式 3.4.0 及以上版本中,R8 是程式碼最佳化和縮減的預設編譯器。不過,您必須啟用 R8。
R8 已取代 ProGuard,但專案根資料夾內的規則檔案仍稱為 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);
}
}
遮蓋記錄中的機密資料
如果記錄中必須包含機密資料,建議您在輸出記錄之前先進行處理,再移除或模糊處理機密資料。方法是使用下列其中一個技巧:
- 權杖化。如果機密資料儲存在保管箱中 (例如透過權杖參照機密資料的加密管理系統),請記錄該權杖而非機密資料。
- 資料遮罩。資料遮罩是無法復原的單向程序。這個程序建立的機密資料版本結構與原始版本類似,但會隱藏欄位中最重要的機密資訊。範例:用「
XXXX-XXXX-XXXX-1313
」取代信用卡號碼「1234-5678-9012-3456
」。在發布正式版應用程式之前,建議您先完成安全性審查程序,以便仔細確認資料遮罩的使用情形。 警告:如果只公開部分機密資料也會大幅影響安全性 (例如處理密碼時),請不要使用資料遮罩功能。 - 遮蓋。遮蓋與遮罩類似,但會隱藏欄位中包含的所有資訊。範例:用「
XXXX-XXXX-XXXX-XXXX
」取代信用卡號碼「1234-5678-9012-3456
」。 - 篩選。如果所選記錄資料庫中的格式字串不存在,請導入該格式,以便修改記錄陳述式中非常數的值。
請只透過「記錄清理器」元件輸出記錄,確保所有記錄在輸出前皆經過處理。如以下程式碼片段所示。
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());
}