日志信息泄露

OWASP 类别: MASVS-STORAGE:存储

概述

日志信息泄露是一种漏洞,应用会将敏感数据打印到设备日志中。如果暴露给恶意攻击者,这些敏感信息本身可能具有价值——例如用户的凭据或个人身份信息 (PII)——或者可能导致进一步的攻击。

此问题可能出现在以下任何场景中

  • 应用生成的日志
    • 日志有意允许未经授权的攻击者访问,但意外地包含了敏感数据。
    • 日志有意包含敏感数据,但意外地可被未经授权的攻击者访问。
    • 通用错误日志,有时可能会打印敏感数据,具体取决于触发的错误消息。
  • 外部生成的日志
    • 外部组件负责打印包含敏感数据的日志。

Android Log.* 语句写入通用内存缓冲区 logcat。从 Android 4.1(API 级别 16)开始,只有特权系统应用才能通过声明 READ_LOGS 权限来获得读取 logcat 的权限。但是,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);
   }
}

删除日志中的敏感数据

如果必须在日志中包含敏感数据,建议在打印日志之前对其进行清理,以移除或模糊敏感数据。为此,可以使用以下技术之一

  • 令牌化。如果敏感数据存储在保管库中,例如可以从其中通过令牌引用密钥的加密管理系统,请记录令牌而不是敏感数据。
  • 数据屏蔽。数据屏蔽是一个单向不可逆的过程。它创建了一个结构上与原始数据类似的敏感数据版本,但隐藏了字段中包含的大多数敏感信息。例如:将信用卡号1234-5678-9012-3456替换为XXXX-XXXX-XXXX-1313。在将应用程序发布到生产环境之前,建议您完成安全审查流程以仔细检查数据屏蔽的使用情况。警告:在即使仅发布部分敏感数据也会对安全性产生重大影响的情况下(例如处理密码时),请勿使用数据屏蔽。
  • 涂黑。涂黑类似于屏蔽,但隐藏了字段中包含的所有信息。例如:将信用卡号1234-5678-9012-3456替换为XXXX-XXXX-XXXX-XXXX
  • 过滤。如果所选日志库中尚不存在格式字符串,请实现格式字符串,以便于修改日志语句中的非常量值。

日志打印应仅通过“日志清理器”组件执行,该组件确保所有日志在打印之前都已清理,如下面的代码片段所示。

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("[email protected]"), 
        "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("[email protected]"), 
        "myname"
    );
    System.out.println(person);
    System.out.println(person.email.getDataToMask());
}