如何用EL表达式防代码注入?JSF中ValueExpression遭Fortify告警
咱们先搞清楚Fortify告警的原因,再一步步解决EL注入的问题。
你的createValueExpression方法接收的ev参数如果是用户可控的输入(比如来自前端表单、URL参数、用户上传的数据),就会触发这个告警。因为EL表达式是会在运行时被解析并执行的,攻击者可以构造恶意的EL代码,比如:
#{T(java.lang.Runtime).getRuntime().exec('rm -rf /')}
这种表达式会直接调用系统命令,造成严重的安全问题。Fortify检测到你把外部输入直接传入了EL表达式创建方法,认为存在代码注入风险。
这里给你几个实用的方案,按优先级排序:
1. 绝对避免用户直接控制EL表达式字符串
最安全的做法是:不要让用户提供完整的EL表达式。所有的EL表达式应该是后端预定义的(比如常量、配置文件里的固定值),或者由后端逻辑生成,完全不依赖用户输入。
比如,如果你的需求是让用户选择某个bean的属性来展示,不要让用户输入#{user.name},而是让用户选择name这个属性名,后端自己拼接成安全的表达式,同时校验属性名的合法性。
2. 对用户可控的表达式部分做严格校验
如果必须用到用户输入作为表达式的一部分(比如属性名、参数),一定要做白名单校验:
- 用正则表达式限制输入只能是字母、数字、下划线,比如
^[a-zA-Z0-9_]+$ - 禁止包含EL的特殊语法:比如
()(方法调用)、T()(访问类)、new(创建对象)、;(多语句)、#{/}(表达式边界,避免用户嵌套表达式)
举个例子,在你的createValueExpression方法里加入校验:
public static final ValueExpression createValueExpression( final FacesContext context, final ELContext elContext, final String ev, final Class classType) { // 校验EL表达式是否符合安全格式,比如只允许#{bean.property}结构 if (!ev.matches("#\\{[a-zA-Z0-9_]+\\.[a-zA-Z0-9_]+\\}")) { throw new IllegalArgumentException("Invalid or unsafe EL expression"); } return context.getApplication().getExpressionFactory() .createValueExpression(elContext, ev, classType); }
3. 配置EL安全限制
大部分EL实现(比如Apache EL,JSF常用的底层EL引擎)都支持安全配置,禁用危险的表达式特性:
通过web.xml配置
添加以下参数来限制EL的权限:
<context-param> <param-name>org.apache.el.parser.SKIP_IDENTIFIER_CHECK</param-name> <param-value>false</param-value> </context-param> <context-param> <param-name>org.apache.el.security.ALLOW_METHOD_INVOCATION</param-name> <param-value>false</param-value> </context-param> <context-param> <param-name>org.apache.el.security.ALLOW_CLASS_LOADING</param-name> <param-value>false</param-value> </context-param>
这些配置会禁止EL调用方法、加载危险类,从根源上减少注入风险。
自定义Secure ELProcessor
如果用的是Tomcat 8+,可以自定义ELProcessor并设置安全规则:
ELProcessor elProcessor = new ELProcessor(); elProcessor.getELManager().addELResolver(new SecureELResolver()); // 或者禁用特定功能 elProcessor.getELManager().setBeanResolver(new RestrictedBeanResolver());
这种方式可以更细粒度地控制EL能访问的类和方法。
4. 避免动态构造EL表达式,直接操作对象
如果你的需求是动态访问bean的属性或方法,尽量不要构造EL字符串,而是直接通过ELContext获取bean实例,然后用反射或getter方法访问:
// 直接获取bean实例 UserBean userBean = (UserBean) elContext.getELResolver().getValue(elContext, null, "userBean"); // 访问属性 String username = userBean.getUsername();
这种方式完全避免了动态EL表达式的风险,比构造字符串更安全。
如果ev参数确实是用户可控的,一定要在方法里加入校验逻辑,拒绝不符合安全格式的表达式。另外,尽量把表达式的生成逻辑放在后端,不要让用户输入完整的表达式。
比如,如果用户只需要指定属性名,你可以修改方法,让用户传入属性名,后端自己拼接成安全的表达式:
public static final ValueExpression createSafeValueExpression( final FacesContext context, final ELContext elContext, final String beanName, final String propertyName, final Class classType) { // 校验beanName和propertyName的合法性 if (!beanName.matches("[a-zA-Z0-9_]+") || !propertyName.matches("[a-zA-Z0-9_]+")) { throw new IllegalArgumentException("Invalid bean or property name"); } String ev = String.format("#{%s.%s}", beanName, propertyName); return context.getApplication().getExpressionFactory() .createValueExpression(elContext, ev, classType); }
内容的提问来源于stack exchange,提问作者Ashutosh Kumar

