1. 什么是日志伪造(Log Forging)?
日志伪造(Log Forging),也叫日志注入(Log Injection),是指攻击者通过向应用程序输入恶意数据,使这些数据被记录到日志中,从而破坏日志的完整性、可读性,甚至执行恶意操作。
假设一个网站记录用户登录日志:
logger.info("用户登录失败: " + username);
如果攻击者输入的用户名是:
admin\n[ERROR] 你的系统已经被攻击,嘿嘿!
那么日志会变成:
用户登录失败: admin
[ERROR] 你的系统已经被攻击,嘿嘿!
这样,攻击者就成功伪造了一条错误日志,可能误导运维人员!运维人员可能查了半天也查不出系统是否真正的被攻击。
2. 日志伪造可能造成的攻击
(1) 日志污染
攻击者可以插入大量垃圾日志,使日志文件变得混乱,难以分析。
例如:注入大量换行符,使日志文件变得巨大,影响存储和查询。
(2) 伪造错误日志
攻击者可以伪造[ERROR]或[WARN]日志,误导管理员采取错误操作。
例如:注入[CRITICAL] 数据库连接失败,让运维误以为系统真的崩溃。
(3) 日志逃逸(Log Injection for Code Execution)
某些日志系统(如ELK、Splunk)支持日志查询语法,攻击者可能注入恶意查询代码。
例如:注入'; DROP TABLE users; --,如果日志被直接导入数据库,可能导致SQL注入。
(4) 隐藏攻击痕迹
攻击者可以注入大量垃圾日志,掩盖真实的攻击行为,使安全审计变得困难。
(5) XSS攻击
日志中被注入一些XSS攻击向量,当日志被显示在日志管理系统时,可能会导致XSS攻击。
3. 如何识别系统是否存在日志伪造问题?
试试在你的Java应用里输入以下内容,看看日志会变成什么样:
String maliciousInput = "hello\n[ERROR] Fake error message!";logger.info("Test log: " + maliciousInput);
如果你的日志里出现了换行和伪造的[ERROR],说明存在日志伪造风险!赶紧修复吧!
4. 如何防御日志伪造?
(1) 使用参数化日志(SLF4J推荐方式)
❌ 错误写法(易受攻击):
logger.info("用户登录: " + username); // 直接拼接字符串,危险!
✅ 正确写法(自动转义特殊字符):
logger.info("用户登录: {}", username); // SLF4J参数化日志,安全!
(2)对日志内容进行过滤或转义
import org.apache.commons.text.StringEscapeUtils;String safeUsername = StringEscapeUtils.escapeJava(username); // 转义换行符等特殊字符
logger.info("用户登录: {}", safeUsername);
(3)配置日志框架过滤特殊字符
Logback 配置示例(自动替换换行符):
<encoder><pattern>%replace(%msg){'[\r\n]', '_'}%n</pattern> <!-- 把换行符替换成下划线 -->
</encoder>
Log4j2 配置示例:
<PatternLayout pattern="%replace{%msg}{[\r\n]}{_}%n"/> <!-- 同样替换换行符 -->
或者
log4j.appender.stdout.layout.ConversionPattern=%encode{%m}%n%
(4)限制日志内容长度
防止攻击者注入超长日志:判断需要写入日志的字符串的长度,并且限制在允许的范围之内,否则,就将字符串截断。
if (username.length() > 100) {username = username.substring(0, 100) + "...(truncated)";
}
logger.info("用户登录: {}", username);
(5) 避免记录敏感或不可信数据
不要直接记录用户输入的完整内容(如HTTP Headers、Cookies)。
对PII信息(如电话、邮件)进行脱敏处理:
logger.info("用户电话: {}", 电话!= null ? "***MASKED***" : "null");
(6) 对需要记录的信息进行转义
import org.apache.commons.text.StringEscapeUtils;// 转义特殊字符
String safeUsername = StringEscapeUtils.escapeJava(username);
logger.info("User login: {}", safeUsername);
(7) 对需要记录的信息进行编码
Import java.net.URLEncoder;
// 转义特殊字符
String safeUsername = URLEncoder.encode(username);
logger.info("User login: {}", safeUsername);
(8) 对输入进行验证
在记录日志之前,对需要记录的字符串进行格式或者取值范围进行验证,如果验证之后的字符串保证不会含有回车和换行字符,也可以避免日志伪造的攻击。 不过,一般建议这么做,因为需要记录日志的地方太多了,一般采用统一配置的方案比较稳妥而且工作量很小。