記錄資訊外洩

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());
}