Checkmarx 9扫描报Log forging日志伪造漏洞问题咨询
方法
getCorrelationId从getHeader元素获取用户输入,该元素的取值未经过正确的安全净化与校验即在代码中流转,最终在startTransaction方法中被用于写入审计日志,可能引发Log forging(日志伪造)风险。
Log Forging问题成因
核心风险逻辑:未经过滤的用户可控输入被直接写入日志时,攻击者可以在输入中插入\r(回车)、\n(换行)等控制字符,人为截断当前日志行,伪造虚假的日志条目(比如伪造管理员操作记录、错误报警信息),干扰审计溯源,甚至绕过SIEM、日志分析系统的检测规则。
当前代码的污点源非常明确:CORRELATION_ID_HEADER请求头完全由客户端可控,攻击者可以随意构造该头的内容传入。
现有代码未通过扫描的原因
代码中使用StringEscapeUtils.escapeJava做处理的方案不满足日志伪造防护要求,Checkmarx不会将其识别为有效净化手段,原因有两点:
StringEscapeUtils.escapeJava的设计目标是转义Java源代码字符串字面量中的特殊字符,并非面向日志注入场景设计,不会针对性拦截日志伪造依赖的CR、LF等控制字符的注入风险,甚至会出现不必要的双重转义问题——代码在getCorrelationId和startTransaction里对同一个值做了两次escapeJava处理,属于冗余逻辑。- 代码完全没有对correlationId做合法性校验:链路追踪ID本身有通用的格式规范(通常为UUID、或仅包含字母/数字/中划线的定长字符串),没有白名单校验的情况下,静态扫描规则会判定输入仍存在被恶意构造的可能。
另外代码存在逻辑隐患:从request取到头的值后没有先判空就直接传入转义方法,虽然commons-lang3的工具方法对null入参做了兼容,但逻辑严谨性不足。
正确修复方案
按照优先级做三层处理即可彻底解决该问题,同时满足Checkmarx的扫描规则要求:
- 白名单格式校验优先:对从请求头获取的correlationId做正则匹配,仅允许符合格式要求的值通过,不符合的值直接丢弃,使用默认生成的合法correlationId替代。
- 针对性移除日志风险字符:放弃使用
escapeJava,专门过滤掉输入中所有ASCII控制字符(字符编码小于0x20的字符,包含\r、\n、退格、换页等所有可能干扰日志格式的控制符)。 - 修正代码逻辑问题:调整空判断顺序,移除重复转义逻辑,所有日志打印统一使用参数化格式,避免无意义的字符串拼接。
修复后参考代码
// 提前定义correlationId白名单正则,根据实际业务的ID格式调整,这里以通用的UUID/链路ID格式为例 private static final Pattern CORRELATION_ID_PATTERN = Pattern.compile("^[a-zA-Z0-9\\-]{1,64}$"); private static final String DEFAULT_CORRELATION_ID = "unknown"; private String getCorrelationId(HttpServletRequest request) { String headerValue = request.getHeader(CORRELATION_ID_HEADER); // 先判空再处理 if (headerValue == null || headerValue.isEmpty()) { return DEFAULT_CORRELATION_ID; } // 移除所有ASCII控制字符,从根源杜绝换行伪造 String sanitized = headerValue.chars() .filter(c -> c >= 0x20) .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append) .toString(); // 白名单格式校验,不匹配则返回默认值 if (CORRELATION_ID_PATTERN.matcher(sanitized).matches()) { return sanitized; } return DEFAULT_CORRELATION_ID; } private void startTransaction(HttpServletRequest request, String serviceName, Object... args) { String correlationId = getCorrelationId(request); // getCorrelationId返回值已经完成净化,无需重复处理 if (DEFAULT_CORRELATION_ID.equals(correlationId)) { logger.info("{} service:{} args:{}", LOG_SERVICE_TYPE, serviceName, args); } else { logger.error("{} service:{} correlationId:{} args:{}", LOG_SERVICE_TYPE, serviceName, correlationId, args); } }
补充说明
不要依赖日志框架的参数化打印能力防范日志伪造:参数化打印仅能解决日志表达式注入(比如早年Log4j2的JNDI注入类问题),不会自动过滤输入中的换行控制字符,必须手动做字符过滤和格式校验才能彻底解决Log Forging风险。
内容的提问来源于stack exchange,提问作者Maveric
相关产品推荐
相关产品推荐

