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