使用ESAPI修复CWE-117时如何保证Java日志可读性
CWE-117是日志注入漏洞,核心风险是攻击者可通过在可控输入中插入CR/LF换行符,伪造日志条目、插入虚假审计记录。ESAPI默认日志实现为了快速封堵这个风险,采用了最简单粗暴的处理逻辑:对输出到日志的所有内容做全量扫描,把所有\r、\n换行符统一替换为__,这一逻辑无差别处理了程序生成的可信异常栈内容,直接把多行堆栈打平成单行,才导致日志完全不可读。
所有方案均保留CWE-117防护能力,不会重新引入漏洞。
方案1:拆分日志内容做分级编码(推荐,零额外依赖)
核心原则是「谁可控谁编码」,不要把整条日志拼接完再扔给ESAPI做全量过滤:- 固定日志文案、JVM抛出的异常堆栈属于程序生成的可信内容,不做换行替换,保留原生格式
- 所有来自用户输入、请求参数、外部接口返回的不可信内容,单独调用编码逻辑过滤CR/LF注入风险:把这部分内容里的换行符替换为可视化转义标记(比如
[LF]、[CR]),既阻断注入,又能保留原输入的特征
参考实现代码:
import java.util.regex.Pattern; public class LogSafeUtils { // 匹配不可信内容里的CR/LF字符 private static final Pattern CRLF_INJECT_PATTERN = Pattern.compile("[\r\n]"); /** * 仅对外部不可信输入做日志注入防护编码 */ public static String encodeUnsafeContent(String unsafeInput) { if (unsafeInput == null) { return "null"; } return CRLF_INJECT_PATTERN.matcher(unsafeInput).replaceAll(match -> match.group().equals("\n") ? "[LF]" : "[CR]" ); } } // 日志调用示例 try { // 业务处理逻辑 } catch (BizException e) { // 仅对外部传入的用户参数做编码,异常对象直接传入保留原生栈换行 String safeParam = LogSafeUtils.encodeUnsafeContent(userInputParam); // 如果你用的是SLF4J/Logback/Log4j,异常作为最后一个参数传入时会自动保留栈格式 log.error("业务处理失败,用户提交参数:{}", safeParam, e); }如果你需要继续使用ESAPI自带的Logger方法,可以重写其内部的编码逻辑:覆盖默认的日志内容编码方法,判断如果是Throwable类型的参数,逐行打印栈内容,仅对栈信息中携带的用户可控消息做过滤,不替换JVM生成的栈换行。
方案2:调整ESAPI配置桥接底层日志框架
如果你当前项目已经在用Log4j2/Logback等成熟日志框架,不需要用ESAPI自带的日志输出实现:- 修改
ESAPI.properties配置,将Logger.UserInfo=false、Logger.LogApplicationName=false这类不需要的全量日志拼接项关闭,把ESAPI的日志实现桥接到项目实际使用的日志框架 - 保留ESAPI的编码能力,但仅用它处理不可信日志参数,关闭ESAPI对整条日志内容的全量CRLF替换逻辑,把
Logger.CRLFReplacement的替换规则从全量生效改为仅对参数部分生效
这种方式下,异常栈的输出完全由底层成熟日志框架控制,会自动保留正确的换行缩进格式,可读性和正常日志没有区别,同时参数部分的ESAPI编码依然能阻断CWE-117注入。
- 修改
方案3:日志查看端做规则适配(临时方案)
如果暂时无法修改线上应用代码,可以调整日志查看的替换规则,不要全局把__替换为换行:仅匹配异常栈特征片段(比如__at、__Caused by:、__... <N> more这类异常栈专属前缀)前的__替换为换行,普通日志内容中的__保留,既可以正常阅读堆栈,也不会误替换日志文本中本来存在的__字符。
注意:绝对不要为了可读性直接关闭ESAPI的日志编码校验,否则会直接重新暴露CWE-117漏洞,给攻击者留下日志伪造的入口。
内容的提问来源于stack exchange,提问作者Robson Braga

