JNDI注入攻击在近几年的安全攻防中出尽了风头,Log4j2的漏洞更是把这种攻击方式推向了大众视野。很多开发者一听到JNDI就紧张,甚至想彻底禁用它。但Tomcat作为一个成熟的Servlet容器,其内部很多功能本身就依赖JNDI来运转,一刀切地禁用并不现实。真正的安全加固思路,是精准地关闭那些你根本用不上的JNDI资源入口,让攻击者找不到可以注入的支点。
攻击者是如何利用JNDI的要理解怎么防,得先知道对方怎么打。JNDI全称是Java命名和目录接口,它允许Java程序通过名称来查找数据或对象。这本是个方便的设计,但问题出在它支持从远程服务器加载对象。攻击者如果能在JNDI的lookup方法里插入一段恶意地址,比如ldap://evil.com/exploit,程序就会傻傻地连过去,把攻击者准备好的恶意类下载到本地并执行。整个过程不需要你代码里有任何明显的漏洞,只要有一个未经验证的输入流进了lookup参数,服务器就沦陷了。这就是远程代码执行的本质。
Tomcat内部使用JNDI的场景比很多人想象中要多。数据库连接池通过JNDI暴露、邮件会话通过JNDI查找、甚至一些自定义的全局配置也会挂载在JNDI树上。这些资源本身是安全的,因为它们是你自己配置的本地对象。真正危险的是那些允许从外部动态绑定对象的入口,以及那些对lookup参数没有做任何限制的代码逻辑。
Tomcat中哪些JNDI资源是可以禁用的Tomcat默认开放了好几种JNDI资源类型,但绝大多数生产环境只用了其中的一小部分。你需要对照自己的项目,把那些完全用不到的资源类型从配置中剔除。下面逐一分析每种资源的使用场景和禁用方法。
第一个要审视的是UserDatabase资源。它通常配置在server.xml的GlobalNamingResources里,用来管理Tomcat的管理员账号。如果你根本不使用Tomcat自带的Manager应用或者Host-Manager应用,这个资源对你来说就是多余的。它默认会从conf/tomcat-users.xml读取用户信息,并暴露在JNDI环境下。攻击者虽然不能直接通过它执行代码,但它提供了用户枚举的可能性,而且在某些组合利用场景下会变成信息泄露的跳板。禁用它很简单,在server.xml中注释掉或者删除对应的Resource元素即可。
第二个需要重点关注的是JavaMailSession资源。很多开发者在配置邮件发送功能时,会在Tomcat的context.xml里定义一个邮件会话资源,然后通过JNDI去获取。如果你的应用根本不发邮件,或者邮件配置完全在应用层通过代码直接设置,那么这个JNDI入口就应该关闭。更关键的是,邮件会话的配置里包含SMTP服务器地址、端口、账号密码等敏感信息,一旦JNDI配置被意外暴露,这些信息就跟着泄露了。
第三个容易被忽视的是JDBC数据源资源。这可能是Tomcat里最常用的JNDI资源类型,但也是配置最混乱的地方。很多项目在初期测试时配置了多个数据源,上线后只用其中一个,其余的都还留在配置文件里。这些废弃的数据源定义不仅占用连接池资源,还增加了攻击面。特别是那些配置了高权限数据库账号的旧数据源,如果被恶意利用来执行SQL注入,后果不堪设想。你需要逐个检查每个context.xml和server.xml中的Resource定义,把不再使用的数据源配置清理干净。
第四个是EJB和远程对象引用资源。如果你的应用是个纯粹的Web项目,没有用到任何EJB组件,那么任何指向远程对象的JNDI资源都应该被禁用。这类资源通常会配置一个factory属性,指向某个远程对象工厂类,攻击者如果能控制这个factory的值,就可以实例化任意类。检查你的配置文件中是否存在带有factory="org.apache.naming.factory.BeanFactory"或者类似远程工厂类的Resource节点,有的话直接删除。
具体配置层面的硬核操作光知道哪些该禁用还不够,你得知道怎么在配置文件里实际操作。Tomcat的JNDI配置分布在几个不同的位置,层级关系需要搞清楚。server.xml里的GlobalNamingResources是全局级别的,所有虚拟主机和应用都能访问到。每个Host元素下的Context元素里可以定义应用级别的资源,META-INF/context.xml里也可以定义。这些配置会叠加生效,所以你需要逐层排查。
下面是一个典型的server.xml中需要清理的配置示例:
<!-- 如果不需要Manager应用,直接删除整个GlobalNamingResources块 -->
<GlobalNamingResources>
<!-- 删除或注释掉UserDatabase -->
<!-- <Resource name="UserDatabase" auth="Container"
type="org.apache.catalina.UserDatabase"
description="User database that can be updated and saved"
factory="org.apache.catalina.users.MemoryUserDatabaseFactory"
pathname="conf/tomcat-users.xml" /> -->
</GlobalNamingResources>
对于应用级别的context.xml,你需要检查每个ResourceLink和Resource元素。很多开发者在调试时会添加一些测试用的资源,上线后忘记删除。这些残留配置是最容易被攻击者利用的。一个干净的context.xml应该只保留业务必需的资源定义,并且每个资源都要明确指定factory属性,避免使用可能被绕过的通用工厂类。
<!-- 一个相对安全的JDBC资源定义示例 -->
<Resource name="jdbc/AppDB"
auth="Container"
type="javax.sql.DataSource"
factory="org.apache.tomcat.jdbc.pool.DataSourceFactory"
driverClassName="com.mysql.cj.jdbc.Driver"
url="jdbc:mysql://localhost:3306/appdb"
username="appuser"
password="encrypted_password"
maxTotal="20"
maxIdle="10"
validationQuery="SELECT 1" />
注意上面的配置中,factory明确指定为Tomcat自带的连接池工厂类,而不是一个通用的BeanFactory。这个细节很重要,因为攻击者常用的手法之一就是通过BeanFactory来实例化任意类并调用其setter方法,从而执行恶意代码。当你把factory限定为具体的、功能单一的工厂类时,攻击者的操作空间就被大幅压缩了。
代码层面如何防御JNDI注入配置层面的清理解决了静态资源暴露的问题,但动态的JNDI查询操作仍然需要代码层面的防护。如果你的应用代码中存在任何形式的JNDI lookup操作,且lookup的参数有一部分来自用户输入,那就必须进行严格的校验。
最直接的防御方式是对lookup的字符串参数做白名单校验。你可以在代码中维护一个允许查询的JNDI名称列表,任何不在列表中的查询请求直接拒绝。这个列表应该只包含java:comp/env/前缀下的本地资源名称,绝对不允许出现ldap、rmi、dns等协议前缀。下面是一个简单的校验逻辑示例:
private static final Set<String> ALLOWED_JNDI_NAMES = Set.of(
"java:comp/env/jdbc/AppDB",
"java:comp/env/mail/AppMail",
"java:comp/env/AppConfig"
);
public Object safeJndiLookup(String jndiName) throws NamingException {
if (jndiName == null || !ALLOWED_JNDI_NAMES.contains(jndiName)) {
throw new SecurityException("JNDI lookup denied for: " + jndiName);
}
// 额外检查是否包含危险协议前缀
if (jndiName.toLowerCase().contains("ldap://") ||
jndiName.toLowerCase().contains("rmi://") ||
jndiName.toLowerCase().contains("dns://")) {
throw new SecurityException("Remote JNDI protocol is not allowed");
}
InitialContext ctx = new InitialContext();
return ctx.lookup(jndiName);
}
如果你使用的是Tomcat 8.5及以上版本,还可以利用JRE内置的JNDI限制机制。在JVM启动参数中添加以下配置,可以直接禁止从远程加载类:
-Dcom.sun.jndi.ldap.object.trustURLCodebase=false -Dcom.sun.jndi.rmi.object.trustURLCodebase=false -Dcom.sun.jndi.cosnaming.object.trustURLCodebase=false
这些参数在JDK 8u121、7u131、6u141之后的版本中默认已经是false了,但显式地设置一遍没有坏处,可以防止有人误改了默认配置。需要注意的是,这些参数只影响远程类加载,不会影响本地JNDI资源的正常使用,所以对业务没有影响。
Tomcat版本升级带来的安全改善Tomcat官方在安全方面一直在改进,新版本对JNDI的限制比旧版本严格得多。如果你还在使用Tomcat 7或者更早的版本,强烈建议升级到Tomcat 9或更高版本。Tomcat 9.0.21之后的版本在JNDI资源处理上增加了更多的安全检查,包括对资源工厂类的类型验证、对远程对象绑定的限制等。
Tomcat 10更是在架构上做了调整,从javax迁移到了jakarta命名空间,虽然这主要是为了兼容Jakarta EE 9,但在这个过程中也重构了部分JNDI处理逻辑,使得攻击面进一步缩小。如果你的项目允许,迁移到Tomcat 10是一个值得考虑的安全投资。
升级版本的同时,别忘了检查Tomcat默认安装后自带的那些示例应用和文档。docs、examples、host-manager、manager这些目录在安装包里是默认存在的,但生产环境完全不需要它们。直接删除webapps下的这些目录,不仅减少了磁盘占用,也消除了这些应用可能带来的JNDI利用路径。特别是manager应用,它本身就是通过JNDI来管理资源的,留着一个不需要的管理入口等于给攻击者多开了一扇窗。
建立JNDI安全审计的常态化机制安全加固不是一次性的工作。项目在迭代过程中,新的依赖可能会引入新的JNDI使用场景,新的开发人员可能会在代码里写下不安全的lookup调用。你需要把JNDI相关的安全检查纳入到代码审计和上线流程中。
一个实用的做法是在CI/CD流水线中加入静态代码扫描规则,专门检测代码中是否存在对InitialContext.lookup的调用,以及这些调用的参数是否可能来自用户输入。同时,定期扫描所有Tomcat配置文件,检查是否有新增的Resource定义,以及这些定义是否合理。把这些检查做成自动化脚本,每次构建时自动运行,可以大幅降低人为疏忽导致的安全风险。
另外,在运行时监控层面,可以通过Java的Security Manager或者自定义的Agent来监控JNDI的lookup操作。一旦检测到有lookup请求指向外部地址,立即触发告警。这种运行时防护虽然有一定的性能开销,但对于关键业务系统来说是值得的。Tomcat本身支持Security Manager模式,虽然配置起来比较繁琐,但它能提供细粒度的权限控制,包括限制JNDI可以访问的协议和地址范围。
最后需要强调的是,禁用不必要的JNDI资源只是整个安全防护体系中的一环。它解决的是攻击面收缩的问题,但无法替代输入校验、权限控制、依赖管理等其他安全措施。一个真正安全的应用,需要从配置、代码、运行时三个层面同时发力,才能有效抵御JNDI注入这类攻击。
