大家好,我是伟伟。Log4j不是有漏洞吗?然后前几天有个朋友来找我问我:我项目里用的是Lombok的@Slf4j,会不会有影响?.png)你说的真巧,我也用了这个注解,所以当时就看了一下。先说结论:有没有影响取决于你项目依赖的log4j2包,和Lombok无关。另外,“提问前请三思而后行,不要浪费我们的时间,不要问自己能想通的问题”这句话不是我说的,而是Lombok的作者:Whydidhesaysuch一个简短的陈述?带着一丝愤怒呢?我给你看。从issue开始,你在github上找到Lombok项目,然后查看它的issue,你会看到关于log4j的问题已经置顶了:所以关于这个issue,我就从这个issue开始,里面有Lombokmaintainers权威解答。https://github.com/projectlom...本期标题翻译为:结论:关于log4j的0day问题,Lombok本身不受影响。注意,这里面有个很耐人寻味的词:本身。一般我们说Lombok不受影响,为什么还要加上它自己(itself)呢?这里有一个故事。首先,12月10日下午5点41分,也就是漏洞被曝光的那个下午,有哥们在Lombok的issue中提出了这个问题:他说:铁哥们,出问题了,log4j爆发了一个巨大的漏洞,赶快补上吧。然后一个叫Rawi01的老铁跳出来说:兄弟,你能解释一下这个漏洞是如何影响Lombok的吗?据我所知,只有在运行测试时没有出现编译错误时才需要log4j。这位老哥显然对Lombok比较了解,他居然指出了重点:Lombok不依赖log4j。但后来有人指出:Lombok提供了一个名为@log4j的注解。这东东用的是Log4J库,毕竟是要用来记录日志的。确实有这个注解。给大家展示一下:我一般使用@Slf4j注解,但是它确实提供了其他的日志注解。先按这个,以后再说。目前的主要任务就是这个问题,再往下看。接下来来了一个控场的哥们,看他怎么说:他先给说@log4j注解的哥们打电话,跟他解释:虽然Lombok会生成代码创建logger实例,但是我们不发货,分发或要求任何特定版本。如果用户想要使用这个注解,需要自己提供相应的依赖。这里我有三个词我还没有翻译:ship、distribution或require。require其实很简单,就是一个要求。就是说我们在使用的时候不需要用户给出指定版本的依赖。说白了,如果要使用@Log4j,那就提供相应的依赖。至于这个依赖的版本,Lombok并不关心。remainingship直译为“交付”,而distribute则有“分发”的意思。让我用一张图来解释一下:正如你所看到的,我在项目中引用了Lombok,但它不会将依赖项传递给任何其他包。它不像spring-boot-starter后面跟着一大堆东西。我理解这就是他想表达的意思:我们不运送、分发或要求任何特定版本。然后他接着说:我们确实可以在我们的代码库中找到一个版本的log4j2,但它只是在测试代码中使用,这样生成的代码才能编译无误。最后,这位哥们总结道:老铁们别着急,Lombok还是可以放心使用的。但是我觉得虽然测试代码中引用了Log4j,但是应该把依赖更新到安全版本。怎么样,是不是觉得这位老者说话的时候,有着一种不容置疑的自信。看完这个回答,我的心情是这样的,隐隐觉得这是个大boss。于是就看了一下这位老哥的背景:想从他的github主页找答案,但是没有找到和Lombok相关的项目。而且只有49个follower,跟boss的数据好像不太吻合:于是我打开浏览器搜索:RoelSpilkerLombok。我发现了这个:我才发现,好家伙,他是龙目岛的爸爸,难怪他话那么多a。还发现了一个花絮:他们之前也给Oracle贡献过关于Lombok的想法,但是被拒绝了。政府不支持,他们只好自己起来做。现在龙目岛的市场占有率还是很高的,也算是成功了。另外,其实在这里可以直接看出他的身份:Collaborator,即合作者。他的回答可以等同于官方的回答。到目前为止,事情发展得比较顺利。官方亲自下来回答了这个问题,并且已经明确表示:可以算是Scattering的结束。不过,我就怕有一个但是,后来发生的事情,我觉得有点离谱。首先,SunriseChair大哥给了一个kill:他先引用了作者的回复,然后说,如果你在maven或者gradle中声明了Lombok依赖,那么Log4j的依赖是不是也会被包含进来??也许这是对类路径的唯一依赖,而Lombok生成的代码也使用它?如果我错了请纠正我……他是什么意思。首先,我觉得看了作者的回答,他认为Lombok依赖Log4j,所以他的核心问题是,如果我引用Lombok,Log4j的依赖会不会也被传入?但是作者说过:在我们的代码库中可以找到一个版本,但是那个版本只在测试类中使用。但是如果你对开源项目的工作原理稍有了解,你也知道不会提供测试相关的部分。如果要看测试类,就得把项目源码拉下来。简单的说,如果通过maven依赖Lombok,肯定是看不到测试相关的东西的。接着一个叫阮努内斯的老者也出来给枪装满了,并提出要秒杀。他说他在整个项目中发现了这个东西:这个配置有漏洞。那么笔者就这两个问题一一进行了解答:首先,听师兄说Lombok依赖log4j:Lombok不依赖log4j,也不依赖任何其他库。如果您在代码中使用@Log4j注释,但不直接或间接依赖log4j,您的编译将生成一条错误消息。关于这一点,之前的依赖分析截图也有说明,这里不再赘述。然后说bugfinder听好了:在与你发现的bug相同的目录中有一个文本文件,但如果你看一下,你就会知道我们不会将该文件作为我们发布的一部分。所以我明白了,根本不会放出来,就算有bug,Lombok用户也没什么错吧?我也去找了作者提到的那个文档,是:https://github.com/projectlom...这个文档写的很清楚:notpartoflombokitself.如果前面两道题,如果已经让笔者觉得有些难受的话,那么接下来的一道题可能就是引爆点,完成三杀,一波带走:这哥们上来就说:老铁,让我问一个问题。我正在使用@slf4j注释,这个错误与我有关吗?不不不,血压升高了。我觉得这哥们根本就没有看之前的回复才问这个问题。如果他在作者给出两个解释之前就问这个问题,那我觉得完全可以理解。但是在作者本期已经解释了两次“Lombok不受影响”的前提下,一炮而红,来了个暴击:大哥,是不是我用的注解有问题?当我写这篇文章时,我的血压升高了一点。这和我之前在群里和别人讨论交易失败的场景很像。讨论到此结束。突然冒出一个哥们,抛出一段代码。这段代码的编写方式是经典的this调用。事务注释失败的场景,这是我们刚才讨论的一部分。知道我们在讨论这个问题,即使你稍微向上滚动一点,在提问之前了解上下文不是很好吗?话不多说,可能是我过度解读了,反正我觉得大家应该学会提问的艺术。回到我们的主线,看看作者是怎么回答这个问题的:他说:老头子,这个问题我帮不了你。我对这个错误一无所知,我什至不明白为什么你会认为它也会影响@Slf4j。我可以告诉你的是,Lombok不会使用、传递、需要这些库的依赖项。我们通过生成您的“隐形源代码”来工作。如果您有任何理由怀疑他们的产品也可能出现类似问题,请联系Slf4j的维护者。不知道是不是个人感觉,我觉得作者在回答这个问题的时候已经有点生气了:这些问题是什么?之前已经回复过两次了,不受影响?我怎么感觉我的项目用户不了解Lombok的基本原理?笔者回复粗心的提问者后,提问者礼貌回复:谢谢老铁,我查过了,我用的是SpringBoot默认的logback。作者也把这个问题置顶并修改了这个问题的标题。所以,经过前面的一些解读,现在你再看看这个标题:作者为什么要强调“自己”,因为Lombok确实提供了日志功能,但是至于引用了哪个包,哪个版本的包,就无所谓了与龙目岛一起做。关系。龙目岛本身(本身)是安全的。最后作者给出了对Lombok的log4j漏洞的最新评估(latestassessment),也算是总结发言了:翻译一下关键的东西给大家。该漏洞仅存在于2.16.0版本以下的Log4j代码包中,其他任何日志框架均不存在。Lombok不传递依赖任何Log4j包,也不声明对任何东西的依赖。如果你使用了任何Lombok注解,比如@Log4j,Lombok会生成使用这些库的代码,但是你的项目必须包含对这些库的依赖,否则Lombok生成的代码将无法通过编译。同样,您有责任在运行时拥有这些包,否则类初始化可能会失败。在Lombok测试代码中,我们曾经有一个包含此漏洞的版本,但由于测试不处理任何用户输入(测试是硬编码的)并且生成的代码甚至没有执行,因此运行测试没有结果在执行测试RCE(远程代码/命令执行漏洞)的机器中。所以老铁们,Lombok本身不需要做任何改动,也不会对你的项目承担任何安全责任,毕竟我们没有引入包。如果您不同意当前的评估,请在此问题上添加评论。但是,请确保您已阅读其他评论并确保您理解问题。最后这两句话,单独来看,我非常喜欢这两句话:提问前请三思,不要浪费我们的时间,不要问自己能弄清楚的问题。如果您认为我们遗漏了什么,或有新信息,请说出来。然后,你注意到作者在这里使用的副标题是:Thebalancingact。翻译过来就是“balancingact”,什么鬼?NO,NO,NO:给大家一点俚语,不客气。补充说明前面已经讲完了主线,现在补充说明一下。先说一下之前没有展示的关于日志的注意事项。其实在Lombok中关于日志的注解还是蛮多的。可以直接看官方文档:https://projectlombok.org/fea...这么多注解,一一讲没意义,我就挑@Let'sdemonstrateSlf4j和@Log4j2吧。首先,我们可以搭建一个只包含这两个依赖的纯SpringBoot工程:如果我这时候什么都不动,我就稍微改一下启动类:然后为了排除干扰项,我调整日志级别打印到Error:logging.level.root=error关闭banner输出:spring.main.banner-mode=offbanner是这样的:此时启动项目,日志输出是这样的:可以看到此时我们使用的日志是logback,原因是我在上一篇文章中也提到过,因为Springboot默认使用的日志实现是logback。这一点从项目依赖也能看出来:另外请注意,我还特意把import部分删掉了。这里除了@Slf4j注解外,没有介绍日志相关的注解。然后,我会注意此时编译的class文件:自动引入slf4j相关的包,然后生成这行代码:privatestaticfinalLoggerlog=LoggerFactory.getLogger(LogdemoApplication.class);不知道大家有没有在编译的时候没想到注解相关的东西,不过大家别慌,我这里还是按下了按钮。来,我问你:为什么能引入slf4j相关的包?因为我依赖它:嗯,如果我这个时候去掉logback的核心依赖,会怎么样,你觉得会不会编译不通过?虽然不会编译,因为Slf4j包仍然存在,它只是一个日志外观。但是运行的时候会抛出异常,因为找不到日志相关的具体实现类:那么,如果我想使用log4j2的日志实现呢?我在之前的文章中也写过:去除Springboot的默认依赖,然后引入Log4j2包。这时候的项目依赖图是这样的。可以看到没有logback-core,只有log4j-core:再次运行项目,日志实现变成了Log4j:有没有发现我改了pom依赖,其他一行代码都没动,日志记录框架已从logback更改为log4j。而且class文件也没有变化,就不截图了。这是Slf4j的功劳,也就是“门面”的意思,这也是为什么推荐大家在项目中使用Slf4j,而不是logback、log4j等具体的日志实现。接下来说一下@Log4j2注解。我们还是把依赖恢复到原来的纯状态,也就是:然后我们把注解改成了@Log4j2,但是此时我们的项目中并没有引入Log4j-core包,所以你觉得会不会有问题?没问题,我们可以看看。先看输出:此时的日志实现类是SLF4JLogger。这东西是从哪里来的?看类文件:这两个类来自log4j-api包,并且因为有log4j-to-slf4j包的存在,所以最终的实现类桥接到SLF4JLogger:如果我去掉log4j-api包,做你认为它不会编译?一定不能编译,因为包不存在,不能产生class文件:如果我不想用SLF4JLogger类,我想用真正的log4j。简单,引入log4j依赖:OK,前面说了那么多废话,不厌其烦的给你排除和引入日志相关的包,并给你看输出,整个过程不涉及Lombok包裹。改的是再次确认这两句话:如果你使用任何Lombok注解,比如@Log4j,Lombok会生成使用这些库的代码,但是你的项目必须包含对这些库的依赖,否则Lombok生成的代码将无法编译。比如我前面去掉了log4j-api包,是不是编译不通过?同样,您有责任在运行时拥有这些包,否则类初始化可能会失败。比如我前面去掉了logback-core这个包,编译的时候没有问题,但是服务运行的时候会不会抛出类找不到的异常?是不是又证明了:在讲原理之前,我提到了“编译时注解”这句话。不知道大家是否知道这个东东。看不懂很正常,因为我们在写业务代码的时候,很少自定义编译时注解。做运行时注解差不多就够了。Lombok的核心工作原理是编译时注解。其实我对它了解不多,只是大概知道它的工作原理是什么,并没有深入研究源码。但是我可以跟大家分享两个需要注意的地方,以及大家可以去哪里学习这个东东。首先首先要注意的地方在这里:日志相关注释的源码就在这部分,看得出来很奇怪,这些文件都是以SCL.lombok结尾的,这是什么?这是lombok的一个小idea。其实这些都是class文件,只是为了避免污染用户工程,做了特殊处理。所以打开这类文件的时候,可以选择以class文件的方式打开,可以看到里面的具体内容。比如你可以看看这个文件:lombok.core.handlers.LoggingFramework,你会发现你写了很多类似枚举的日志实现:每个注解需要产生的日志都是硬编码的。也正是因为如此,Lombok才知道你使用了什么样的日志注解,应该为你生成什么样的日志。例如,log4j是这样的:privatestaticfinalorg.apache.logging.log4j.Loggerlog=org.apache.logging.log4j.LogManager.getLogger(TargetType.class);这也可以对应我们之前的类文件:而SLF4J是这样的:privatestaticfinalorg.slf4j.Loggerlog=org.slf4j.LoggerFactory.getLogger(TargetType.class);第二个要注意的是找到入口:这些类文件的入口就是在这个地方加载的,它是基于Java的SPI机制:AnnotationProcessorHider这个类有两行静态内部类,我们看其中的一个,AnnotationProcessor,继承自AbstractProcessor抽象类:javax.annotation.processing.AbstractProcessor这个抽象类是入门中的入门,核心中的核心。在这个条目中,初始化了一个名为ShadowClassLoader的类加载器:它的作用是加载那些标记为SCL.lombok的类文件。那我怎么知道Lombok是基于编译时注解的呢?其实这个东西我看过的两本书里都有写,印象很模糊。写文章的时候翻出来看了一遍。首先是《深入理解 Java 虚拟机(第三版)》程序编译与代码优化:前端编译与优化第四篇第10章。里面有一节专门讲插件注解:Lombok的主要工作点在javac编译过程中。在书的第361页,提到了编译过程的几个阶段。从Java代码的整体结构来看,编译过程大致可以分为一个准备过程和三个处理过程:1.准备过程:初始化插件注解处理器。2、符号表的解析和填充过程,包括:词法和语法分析。将源代码的字符流转化为一组token,构建抽象语法树。填充符号表。生成符号地址和符号信息。3、插件注解处理器的注解处理过程:插件注解处理器的执行阶段,本章实践部分将设计一个插件注解处理器来影响Javac的编译行为。4.分析和字节码生成过程,包括:注解检查。对有关语法的静态信息执行检查。数据流和控制流分析。检查程序的动态运行过程。分解语法糖。将简化代码编写的语法糖恢复到原来的形式。(java中的语法糖包括泛型、变长参数、自动装箱拆箱、遍历循环foreach等,JVM运行时不支持这些语法,需要在编译阶段恢复。)字节码生成。将前面步骤生成的信息转换成字节码。如果说javac编译进程是Lombok的工作站点,那么“插件注解处理器的注解处理进程”就是它的工作站。书中也提到了Lombok的工作原理:.png)第二本书是《深入理解 JVM 字节码》。在它的第八章里面,也详细的描述了插件注解的处理原理,里面也提到了Lombok:.png)最后我画了一个示意图,是这样的:.png)如果看懂了里面的描述书的前十页,然后这张图会更清晰。简而言之,Lombok的核心原理是在编译期间修改类文件,这会帮助你生成大量代码。这也是作者所说的:invisiblesourcecode,看不见的源代码。这里的不可见是指java文件中的不可见,到class文件中还是无处遁形。如果你有兴趣深入了解它的原理,可以看我前面提到的两本书,里面有动手实践开发。我不会写,一个原因是因为门槛确实高,写的生涩难懂。另一个原因不是因为我懒惰。本文已收录在我的个人博客中,欢迎大家来玩:https://www.whywhy.vip/
