OWASP 类别: MASVS-STORAGE: Storage
概览
日志信息泄露是一种漏洞,即应用将敏感数据打印到设备日志中。如果这些敏感信息暴露给恶意行为者,这些信息本身可能很有价值(例如用户的凭据或个人身份信息 (PII)),或者可能促成进一步的攻击。
此问题可能发生在以下任何场景中:
- 应用生成的日志
- 日志有意允许未经授权的行为者访问,但意外地包含了敏感数据。
- 日志有意包含敏感数据,但由于意外而对未经授权的行为者开放访问。
- 通用错误日志,根据触发的错误消息,有时可能会打印敏感数据。
- 外部生成的日志
- 外部组件负责打印包含敏感数据的日志。
Android Log.*
语句写入到公共内存缓冲区 logcat
。自 Android 4.1(API 级别 16)起,只有特权系统应用才能通过声明 READ_LOGS
权限来获得读取 logcat
的权限。然而,Android 支持种类繁多的设备,其预装应用有时会声明 READ_LOGS
权限。因此,不建议直接记录到 logcat
,因为它更容易发生数据泄露。
确保在应用的非调试版本中,所有记录到 logcat
的日志都经过净化。删除任何可能敏感的数据。作为额外的预防措施,使用 R8 等工具删除除警告和错误之外的所有日志级别。如果您需要更详细的日志,请使用内部存储并直接管理您自己的日志,而不是使用系统日志。
影响
日志信息泄露漏洞的严重程度取决于上下文和敏感数据的类型。总的来说,此类漏洞的影响是可能泄露个人身份信息 (PII) 和凭据等关键信息的机密性。
缓解措施
通用
作为设计和实施期间的一般性预防措施,根据最小权限原则绘制信任边界。理想情况下,敏感数据不应跨越或超出任何信任区域。这加强了权限分离。
不要记录敏感数据。尽可能只记录编译时常量。您可以使用 ErrorProne 工具进行编译时常量注解。
避免打印可能包含意外信息(包括敏感数据,具体取决于触发的错误)的日志语句。日志和错误日志中打印的数据应尽可能只包含可预测的信息。
避免记录到 logcat
。这是因为记录到 logcat
可能会由于拥有 READ_LOGS
权限的应用而成为隐私问题。它也无效,因为它无法触发警报或进行查询。我们建议应用仅针对开发者版本配置 logcat
后端。
大多数日志管理库允许定义日志级别,这使得在调试和生产日志之间记录不同数量的信息成为可能。在产品测试结束后,立即将日志级别更改为不同于“debug”。
尽可能从生产环境中移除日志级别。如果无法避免在生产环境中保留日志,请从日志语句中移除非常量变量。可能会出现以下场景:
- 您能够从生产环境中移除所有日志。
- 您需要在生产环境中保留警告和错误日志。
对于这两种情况,使用 R8 等库自动移除日志。任何手动移除日志的尝试都容易出错。作为代码优化的一部分,R8 可以设置为安全地移除您希望保留用于调试但在生产环境中剥离的日志级别。
如果您要在生产环境中进行日志记录,请准备可以在发生事件时有条件地关闭日志记录的标志。事件响应标志应优先考虑:部署的安全性;部署的速度和便捷性;日志编辑的彻底性;内存使用量以及扫描每条日志消息的性能成本。
使用 R8 从生产版本中剥离到 logcat 的日志。
在 Android Studio 3.4 或 Android Gradle plugin 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("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());
}