摘要:当web工程比较大,历史代码较多时, 应当使用log4j2框架的能力来修改日志注入问题,而不是按照有些博文里写的逐个进化参数的方式。

案例故事

某个新系统上线了,小A在其中开发了个简单的登录模块,会在日志里记录所有登录成功或者失败的用户。

小A对用户名都做了白名单校验,不正确的名字,也会用WARN的形式,打印出来做记录。

像下面这样:

  1. [2021-04-17 16:50:35][INFO][main] [Login:308] login success,userName=tony
  2. [2021-04-17 16:50:35][WARN][main] [Login:308] username is wrong,userName=tony.dssdff

日志对接了风险审计系统,会定期从日志中审计出那些每天有可疑登录行为的人,例如那些半夜登录或者频繁登录(不要在意细节,不用审计也能做,只是举个例子而已)

某天,日志审计系统提示tony登录过于频繁且高危操作, 于是把tony的号给封了。

随后一天又封了N多个无辜的用户,引发用户大量不满。运营部找来问罪,小A拿出下面的日志文件做证据:

  1. [2021-04-17 16:50:35][INFO][main] [Login:308] login success,userName=tony
  2. [2021-04-17 16:50:35][WARN][main] [Login:308] username is wrong,userName=tony.dssdff
  3. [2021-04-17 16:50:35][INFO][main] [Login:308] login success,userName=tony
  4. [2021-04-17 16:50:35][INFO][main] [Login:308] login success,userName=tony
  5. [2021-04-17 16:50:35][INFO][main] [Login:308] login success,userName=tony
  6. [2021-04-17 16:50:35][INFO][main] [Login:308] login success,userName=tony

然而tony反应说他那天在外面旅游,电脑也放在家中,是有证据的。

这时候小A的老大翻出了请求接口日志,发现那时候有1个请求发来, 接口里的username参数竟然是:

  1. username=tony.dssdff
  2. [2021-04-17 16:50:35][INFO][main] [Login:308] login success,userName=tony
  3. [2021-04-17 16:50:35][INFO][main] [Login:308] login success,userName=tony
  4. [2021-04-17 16:50:35][INFO][main] [Login:308] login success,userName=tony
  5. [2021-04-17 16:50:35][INFO][main] [Login:308] login success,userName=tony

好家伙,竟然是username里带了换行,虽然我做了白名单校验,但是日志里为了记录这个带换行的错误名,坑了一堆用户。(因为对方可能是使用rest-api去恶意发送的,所以也绕过了前台页面的校验)

小A的公司因此遭遇了巨大损失,小A最终也失业了。

简单整改方法

小A费劲九牛二虎之力找到一家新公司,接手了一堆旧代码。他决定提前预防, 给外部输入的日志参数加上换行处理.

他写了一个方法如下:

  1. /**
  2. * 获取净化后的消息,过滤掉换行,避免日志注入
  3. * @param message
  4. * @return
  5. */
  6. public static String getCleanedMsg(String message) {
  7. if (message == null) {
  8. return "";
  9. }
  10.  
  11. message = message.replace('\n', '_').replace('\r', '_');
  12. return message;
  13. }

并且给自己打日志的地方,补充了这个方法

  1. LOGGER.warn("username is wrong,userName={}", getCleanedMsg(userName));

但是想起来这个系统比较旧,还有好多类似的参数,于是搜索了一下,发现竟然有一千多处带参数的日志,好多是前辈留给他的坑。

于是他怀着责任心一个一个修改和检查, 花了一个多月终于把所有外部输入的参数排查出来并加上getCLeanMsg方法。年末最终因为输出不够,背了个最低绩效,郁郁寡欢,头发又掉光了。

log4j2配置统一修改message

小A被换了个项目组,这次决定不再重蹈覆辙,使用别的方式简化一下。他的项目里日志都是用log4j2打印的,如果能利用框架能力,把日志的换行全部去掉就好了,严格保证日志输出的只有1行。

于是开始认真学习log4j2的官方文档。他在里面找到了和日志输出格式有关的位置,如下:https://logging.apache.org/log4j/2.x/manual/layouts.html

他搜索\n或者换行的关键字,找到了如下的内容:

文档里写得很清楚, 使用%enc{%m}{CRLF}, 即可对这部分进行换行的过滤处理。于是在log4j2.xml的<PatternLayout>改成了如下:

  1. <Console name="Console" target="SYSTEM_OUT">
  2. <PatternLayout pattern="[%d{yyyy-MM-dd HH:mm:ss.SSS}][%-5p] [%t] [%c{10}#%M:%L] %enc{%m}{CRLF} %n "/>
  3. </Console>

测试,最终所有的日志都会只有一行。以前会引发问题的日志也变成了

  1. username=tony.dssdff\r\n[2021-04-17 16:50:35][INFO][main] [Login:308] login success,userName=tony

因此不会被日志系统错误解析,同时也省去了一个个排查的风险。

log4j2 修改异常里的mesage

过了一个月,突然日志审计又告警了, 最终排查下来又是误报。去看了日志,发现长这样:

  1. [2021-04-17 16:50:35][INFO][main] [Login:308] unknown error happend
  2. java.lang.RuntimeException: name,name=%s
  3. [2021-04-17 16:50:35][INFO][main] [Login:308] login success,userName=tony
  4. [2021-04-17 16:50:35][INFO][main] [Login:308] login success,userName=tony
  5. [2021-04-17 16:50:35][INFO][main] [Login:308] login success,userName=tony
  6. [2021-04-17 16:50:35][INFO][main] [Login:308] login success,userName=tony
  7. at java.net.SocketInputStream.socketRead0(Native Method) ~[?:?]
  8. at java.net.SocketInputStream.socketRead(SocketInputStream.java:115) ~[?:?]
  9. at java.net.SocketInputStream.read(SocketInputStream.java:168) ~[?:?]
  10. at java.net.SocketInputStream.read(SocketInputStream.java:140) ~[?:?]
  11. at sun.security.ssl.SSLSocketInputRecord.read(SSLSocketInputRecord.java:448) ~[?:?]

好家伙,原来是有些地方打印日志时, 顺便把未处理过的异常堆栈也打印出来了。异常堆栈的第一行往往是异常名+message, 这里也能被恶意攻击。

小A翻遍了log4j2文档,没有找到能在异常中处理换行的符号,只找到了1个ThrowablePatternConverter, 文档里告诉他,你可以自定义这个ThrowablePatternConverter,来打印自己想要的异常。

于是他自己编写了一个UndefineThrowablePatternConvert,在里面重写了日志堆栈打印的逻辑,

  1. /**
  2. * 会对异常做特定编码处理的格式转换类
  3. * 使用时,在layout中添加 %eEx即可
  4. *
  5. * @since 2021/4/16
  6. */
  7. @Plugin(name = "UndefineThrowablePatternConverter", category = PatternConverter.CATEGORY)
  8. // 自己定义的layout键值
  9. @ConverterKeys({"uEx"})
  10. public class UndefineThrowablePatternConverter extends ThrowablePatternConverter {
  11.  
  12. /**
  13. * 进行过特定编码处理的ThrowableProxy
  14. */
  15. static class EncodeThrowableProxy extends ThrowableProxy {
  16. public EncodeThrowableProxy(Throwable throwable) {
  17. super(throwable);
  18. }
  19.  
  20. // 将\r和\n进行编码,避免日志注入
  21. @Override
  22. public String getMessage() {
  23. String encodeMessage = super.getMessage().replaceAll("\r", "\\\\r").replaceAll("\n", "\\\\n");
  24. return encodeMessage;
  25. }
  26. }
  27.  
  28. protected UndefineThrowablePatternConverter(Configuration config, String[] options) {
  29. super("UndefineThrowable", "throwable", options, config);
  30. }
  31.  
  32. // log4j2中使用反射调用newInstance静态方法进行构造,因此必须要实现这个方法。
  33. public static UndefineThrowablePatternConverter newInstance(final Configuration config, final String[] options) {
  34. return new UndefineThrowablePatternConverter(config, options);
  35. }
  36.  
  37. @Override
  38. public void format(final LogEvent event, final StringBuilder toAppendTo) {
  39. Throwable throwable = event.getThrown();
  40. if (throwable == null) {
  41. return;
  42. }
  43. // 使用自定义的EncodeThrowableProxy,里面重写了ThrowableProxy的getMessage方法
  44. EncodeThrowableProxy proxy = new EncodeThrowableProxy(throwable);
  45. // 添加到toAppendTo
  46. proxy.formatExtendedStackTraceTo(toAppendTo, options.getIgnorePackages(), options.getTextRenderer(), getSuffix(event), options.getSeparator());
  47. }
  48. }

并且在PatternLayout中添加%uEx, 就会使用这里的format去生成堆栈字符串。

总结

  • 白名单无法避免日志注入问题,因为有时候我们可能会记录那些有错误的输入参数。
  • 当web工程比较大,历史代码较多时, 应当使用log4j2框架的能力来修改日志注入问题,而不是按照有些博文里写的逐个进化参数的方式
  • 异常堆栈里的message同样有日志注入风险,如果工程里支持打印堆栈,则最好也统一处理一下。

本文分享自华为云社区《Java云服务开发安全问题解析——日志注入,并没那么简单》,原文作者:breakDraw。

点击关注,第一时间了解华为云新鲜技术~

安全开发Java:日志注入,并没那么简单的更多相关文章

  1. java日志之slf4j与logback简单使用

    最近在开发遇到日志是使用slf4j与logback.xml的配置,所以就记录下来了. 1.导入这几个jar包: Logback 分为三个模块:logback-core,logback-classic, ...

  2. Java日志管理

    首页 资讯 精华 论坛 问答 博客 专栏 群组 更多 ▼ 您还未登录 ! 登录 注册 JavaCrazyer的ItEye(codewu.com)技术博客   博客 微博 相册 收藏 留言 关于我   ...

  3. Java日志框架那些事儿

    文章首发于[博客园-陈树义],点击跳转到原文Java日志框架那些事儿. 在项目开发过程中,我们可以通过 debug 查找问题.而在线上环境我们查找问题只能通过打印日志的方式查找问题.因此对于一个项目而 ...

  4. java日志规约及配置示例终极总结

    目录 什么是日志 常用日志框架 日志级别详解 日志的记录时机 日志使用规约 logback 配置示例 loh4j2 配置示例 什么是日志? 简单的说,日志就是记录程序的运行轨迹,方便查找关键信息,也方 ...

  5. java 日志体系(三)log4j从入门到详解

    java 日志体系(三)log4j从入门到详解 一.Log4j 简介 在应用程序中添加日志记录总的来说基于三个目的: 监视代码中变量的变化情况,周期性的记录到文件中供其他应用进行统计分析工作: 跟踪代 ...

  6. Python GUI之tkinter窗口视窗教程大集合(看这篇就够了) JAVA日志的前世今生 .NET MVC采用SignalR更新在线用户数 C#多线程编程系列(五)- 使用任务并行库 C#多线程编程系列(三)- 线程同步 C#多线程编程系列(二)- 线程基础 C#多线程编程系列(一)- 简介

    Python GUI之tkinter窗口视窗教程大集合(看这篇就够了) 一.前言 由于本篇文章较长,所以下面给出内容目录方便跳转阅读,当然也可以用博客页面最右侧的文章目录导航栏进行跳转查阅. 一.前言 ...

  7. 走进JavaWeb技术世界9:Java日志系统的诞生与发展

    本文转自[码农翻身] ## 一个著名的日志系统是怎么设计出来的? # 1前言 Java帝国在诞生之初就提供了集合.线程.IO.网络等常用功能,从C和C++领地那里吸引了大量程序员过来加盟,但是却有意无 ...

  8. java日志学习(持续更新)

    1.Java实现日志 java日志体系大体可以分为三个部分:日志门面接口.桥接器.日志框架具体实现.原生日志实现(http://www.importnew.com/16331.html) Java日志 ...

  9. Java日志体系居然这么复杂?——架构篇

    本文是一个系列,欢迎关注 日志到底是何方神圣?为什么要使用日志框架? 想必大家都有过使用System.out来进行输出调试,开发开发环境下这样做当然很方便,但是线上这样做就有麻烦了: 系统一直运行,输 ...

随机推荐

  1. django学习-8.django模板继承(block和extends)

    1.前言 django模板继承的作用:模板可以用继承的方式来实现复用,减少冗余内容. 一般来说,一个网站里一般存在多个网页的头部和尾部内容都是一致的,我们就可以通过模板继承来实现复用. 父模板用于放置 ...

  2. nginx判断状态脚本

    A是nginx行数 为0则启动nginx 启动失败则杀死keepalived进程

  3. vue:表格中多选框的处理

    效果如下: template中代码如下: <el-table v-loading="listLoading" :data="list" element-l ...

  4. Jump Server在docker中安装部署

    安装部署: 1.准备机器: 官方环境要求: 硬件配置: 2个CPU核心, 4G 内存, 50G 硬盘(最低) 操作系统: Linux 发行版 x86_64 Python = 3.6.x Mysql S ...

  5. 【ZeyFraのJavaEE开发小知识01】@DateTimeFomat和@JsonFormat

    @DateTimeFormat 所在包:org.springframework.format.annotation.DateTimeFormat springframework的注解,一般用来对Dat ...

  6. C#日志使用

    本文参考链接 日志框架 框架选择:NLog 安装方法,Nuget命令行:Install-Package NLog 常用规则 尽量不要在循环中打印日志. 应输出错误的堆栈信息:e.Message仅为异常 ...

  7. 后端程序员之路 16、信息熵 、决策树、ID3

    信息论的熵 - guisu,程序人生. 逆水行舟,不进则退. - 博客频道 - CSDN.NEThttp://blog.csdn.net/hguisu/article/details/27305435 ...

  8. 《C++ Primer》笔记 第12章 动态内存

    shared_ptr和unique_ptr都支持的操作 解释 shared_ptr sp或unique_ptr up 空智能指针,可以指向类型为T的对象 p 将p用作一个条件判断,若p指向一个对象,则 ...

  9. 【Arduino学习笔记04】消抖动的按键切换

    "开关抖动": 由于按键是基于弹簧-阻尼系统的机械部件,所以当按下一个按键时,读到的信号并不是从低到高,而是在高低电平之间跳动几毫秒之后才最终稳定. 代码解读: 1 const i ...

  10. MMA CTF 2nd 2016-greeting

    目录 MMA CTF 2nd 2016-greeting 总结 题目分析 checksec 函数分析 漏洞点 知识点 利用思路 EXP 完整Exp MMA CTF 2nd 2016-greeting ...