OWASP 类别: MASVS-STORAGE: 存储
概述
日志信息泄露 是一种漏洞,应用会将敏感数据打印到设备日志中。如果泄露给恶意攻击者,这些敏感信息本身可能具有价值——例如用户的凭据或个人身份信息 (PII)——或者可能导致进一步的攻击。
此问题可能出现在以下任何场景中
- 应用生成的日志
- 日志有意允许未授权的攻击者访问,但它们意外包含敏感数据。
- 日志有意包含敏感数据,但它们意外可被未授权的攻击者访问。
- 通用错误日志,有时可能会打印敏感数据,具体取决于触发的错误消息。
- 外部生成的日志
- 外部组件负责打印包含敏感数据的日志。
Android Log.*
语句写入公共内存缓冲区 logcat
。从 Android 4.1(API 级别 16)开始,只有特权系统应用才能被授予读取 logcat
的权限,方法是声明 READ_LOGS
权限。但是,Android 支持种类繁多的设备,其预加载的应用有时会声明 READ_LOGS
权限。因此,不建议直接记录到 logcat
,因为它更容易导致数据泄露。
确保在应用的非调试版本中对所有写入 logcat
的日志进行清理。删除任何可能敏感的数据。作为额外的预防措施,使用 R8 等工具删除除警告和错误之外的所有日志级别。如果您需要更详细的日志,请使用内部存储并直接管理您自己的日志,而不是使用系统日志。
影响
日志信息泄露漏洞类的严重性可能会有所不同,具体取决于上下文和敏感数据的类型。总的来说,此漏洞类的影响是可能导致机密信息(例如 PII 和凭据)的泄露。
缓解措施
常规
作为设计和实施过程中的常规预防措施,根据 最小权限原则 绘制信任边界。理想情况下,敏感数据不应跨越或到达任何信任区域之外。这加强了权限分离。
不要记录敏感数据。尽可能仅记录编译时常量。您可以使用 ErrorProne 工具 进行编译时常量注释。
避免打印可能包含意外信息的日志,包括敏感数据,具体取决于触发的错误。尽可能使日志和错误日志中打印的数据仅包含可预测的信息。
避免记录到logcat
。这是因为记录到logcat
可能会由于拥有READ_LOGS
权限的应用而导致隐私问题。它也不有效,因为它无法触发警报或被查询。我们建议应用程序仅在开发者构建版本中配置logcat
后端。
大多数日志管理库允许定义日志级别,这允许在调试和生产日志之间记录不同数量的信息。在产品测试结束后,立即更改日志级别,使其与“debug”不同。
尽可能从生产环境中移除日志级别。如果您无法避免在生产环境中保留日志,请从日志语句中移除非常量变量。可能会出现以下情况
- 您可以从生产环境中移除所有日志。
- 您需要在生产环境中保留警告和错误日志。
对于这两种情况,请使用 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());
}