乐趣区

关于java:安全开发Java日志注入并没那么简单

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

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

案例故事

某个新零碎上线了,小 A 在其中开发了个简略的登录模块,会在日志里记录所有登录胜利或者失败的用户。

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

像上面这样:

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

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

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

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

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

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

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

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

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

小 A 的公司因而遭逢了巨大损失,小 A 最终也就业了。

简略整改办法

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

他写了一个办法如下:

  /**
     * 获取污染后的音讯,过滤掉换行,防止日志注入
     * @param message
     * @return
     */
    public static String getCleanedMsg(String message) {if (message == null) {return "";}

        message = message.replace('\n', '_').replace('\r', '_');
        return message;
    }

并且给本人打日志的中央,补充了这个办法

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

然而想起来这个零碎比拟旧,还有好多相似的参数,于是搜寻了一下,发现居然有一千多处带参数的日志,好多是前辈留给他的坑。

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

log4j2 配置对立批改 message

小 A 被换了个项目组,这次决定不再吃一堑; 长一智,应用别的形式简化一下。他的我的项目里日志都是用 log4j2 打印的,如果能利用框架能力,把日志的换行全副去掉就好了,严格保障日志输入的只有 1 行。

于是开始认真学习 log4j2 的官网文档。他在外面找到了和日志输入格局无关的地位,如下:https://logging.apache.org/lo…

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

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


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

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

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

因而不会被日志零碎谬误解析,同时也省去了一个个排查的危险。

log4j2 批改异样里的 mesage

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

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

好家伙,原来是有些中央打印日志时,顺便把未解决过的异样堆栈也打印进去了。异样堆栈的第一行往往是异样名 +message,这里也能被歹意攻打。

小 A 翻遍了 log4j2 文档,没有找到能在异样中解决换行的符号,只找到了 1 个 ThrowablePatternConverter,文档里通知他,你能够自定义这个 ThrowablePatternConverter,来打印本人想要的异样。

于是他本人编写了一个 UndefineThrowablePatternConvert,在外面重写了日志堆栈打印的逻辑,

/**
 * 会对异样做特定编码解决的格局转换类
 * 应用时,在 layout 中增加 %eEx 即可
 *
 * @since 2021/4/16
 */
@Plugin(name = "UndefineThrowablePatternConverter", category = PatternConverter.CATEGORY)
// 本人定义的 layout 键值
@ConverterKeys({"uEx"})
public class UndefineThrowablePatternConverter extends ThrowablePatternConverter {
 
      /**
     * 进行过特定编码解决的 ThrowableProxy
     */
    static class EncodeThrowableProxy extends ThrowableProxy {public EncodeThrowableProxy(Throwable throwable) {super(throwable);
        }

        // 将 \r 和 \n 进行编码,防止日志注入
        @Override
        public String getMessage() {String encodeMessage = super.getMessage().replaceAll("\r", "\\\\r").replaceAll("\n", "\\\\n");
            return encodeMessage;
        }
    }
 
    protected UndefineThrowablePatternConverter(Configuration config, String[] options) {super("UndefineThrowable", "throwable", options, config);
    }
 
      // log4j2 中应用反射调用 newInstance 静态方法进行结构,因而必须要实现这个办法。public static UndefineThrowablePatternConverter newInstance(final Configuration config, final String[] options) {return new UndefineThrowablePatternConverter(config, options);
    }

    @Override
    public void format(final LogEvent event, final StringBuilder toAppendTo) {Throwable throwable = event.getThrown();
        if (throwable == null) {return;}
        // 应用自定义的 EncodeThrowableProxy,外面重写了 ThrowableProxy 的 getMessage 办法
        EncodeThrowableProxy proxy = new EncodeThrowableProxy(throwable);
         // 增加到 toAppendTo
          proxy.formatExtendedStackTraceTo(toAppendTo, options.getIgnorePackages(), options.getTextRenderer(), getSuffix(event), options.getSeparator());
    }
}

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

总结

  • 白名单无奈防止日志注入问题,因为有时候咱们可能会记录那些有谬误的输出参数。
  • 当 web 工程比拟大,历史代码较多时,该当应用 log4j2 框架的能力来批改日志注入问题,而不是依照有些博文里写的一一进化参数的形式
  • 异样堆栈里的 message 同样有日志注入危险,如果工程里反对打印堆栈,则最好也对立解决一下。

点击关注,第一工夫理解华为云陈腐技术~

退出移动版