现在再议Struts2 怕是吸引不了多少看官的眼球,但是这个坑我觉得是对惯性思维的挑战,并不是一点营养都没有。
对于输入的净化,一般我们认为最小化限制会更加安全可靠。比如对于Java这种强类型语言,使用int接受参数比String接受参数更加窄化了输入字符空间,自然在防御XSS、SQLi、命令注入等漏洞更加可靠。
这里要说的坑就是在Struts2中即便使用int(其他简单类型也相似)接受参数,在视图中仍然可能输出String类型,因此会存在XSS的隐患。
简单模拟一个根据商品id查询商品信息并将商品信息在页面中输出。
Action
public class ProductAction extends ActionSupport{
private int id; //
@Override
public String execute() {
Product product = null; //模拟查询,结果为空
ServletActionContext.getRequest().setAttribute("target", product);
return SUCCESS;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
}
struts-product.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 2.5//EN"
"http://struts.apache.org/dtds/struts-2.5.dtd">
<struts>
<package name="default" extends="struts-default" >
<interceptors>
<interceptor-stack name="customizedStack">
<interceptor-ref name="exception" />
<interceptor-ref name="alias" />
<interceptor-ref name="servletConfig" />
<interceptor-ref name="i18n" />
<interceptor-ref name="prepare" />
<interceptor-ref name="chain" />
<interceptor-ref name="scopedModelDriven" />
<interceptor-ref name="modelDriven" />
<interceptor-ref name="fileUpload" />
<interceptor-ref name="checkbox" />
<interceptor-ref name="datetime" />
<interceptor-ref name="multiselect" />
<interceptor-ref name="staticParams" />
<interceptor-ref name="actionMappingParams" />
<interceptor-ref name="params" />
<interceptor-ref name="conversionError" />
<interceptor-ref name="validation">
<param name="excludeMethods">input,back,cancel,browse</param>
</interceptor-ref>
<interceptor-ref name="debugging" />
<interceptor-ref name="deprecation" />
</interceptor-stack>
</interceptors>
</package>
<package name="product" extends="default" namespace="/">
<default-interceptor-ref name="customizedStack" />
<action name="productInfo" class="demo.action.ProductAction">
<result name="success">/WEB-INF/pages/jsp/productInfo.jsp</result>
</action>
</package>
</struts>
productInfo.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib prefix="s" uri="/struts-tags"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Product Information</title>
</head>
<body>
<h1>Product Information</h1>
<s:if test="%{target==null}">
Sorry, Product with id:<strong> ${id}</strong> not found!
</s:if>
<s:else>
Product with id: <strong>${id}</strong> found:
<div>
ID :${id}</br>
Name :${name}</br>
Price :${price}</br>
Description:${description}</br>
</div>
</s:else>
</body>
id=1
id=xianzhi
id=<svg onload=alert('Oops')>
(仍以上面的Demo为例子简单解释)
1)大家知道,在Struts2 中有众多的Interceptor,其中com.opensymphony.xwork2.interceptor.ParametersInterceptor会找到参数对应的setter。当客户端传递的参数值是String类型(例如“xianzhi”),尝试从ProdcutAction中寻找 void setId(String id),不幸的是并没有找到(只有void setId(int id),因此会出现错误。
2)另一个Interceptor--com.opensymphony.xwork2.interceptor.ConversionErrorInterceptor 会将id String类型的参数值保存在Map中。
//ConversionErrorInterceptor#intercept
public String intercept(ActionInvocation invocation) throws Exception {
ActionContext invocationContext = invocation.getInvocationContext();
Map<String, Object> conversionErrors = invocationContext.getConversionErrors();
ValueStack stack = invocationContext.getValueStack();
HashMap<Object, Object> fakie = null;
for (Map.Entry<String, Object> entry : conversionErrors.entrySet()) {
String propertyName = entry.getKey();
Object value = entry.getValue();
if (shouldAddError(propertyName, value)) {
String message = XWorkConverter.getConversionErrorMessage(propertyName, stack);
Object action = invocation.getAction();
if (action instanceof ValidationAware) {
ValidationAware va = (ValidationAware) action;
va.addFieldError(propertyName, message);
}
if (fakie == null) {
fakie = new HashMap<Object, Object>();
}
fakie.put(propertyName, getOverrideExpr(invocation, value));
}
}
if (fakie != null) {
// if there were some errors, put the original (fake) values in place right before the result
stack.getContext().put(ORIGINAL_PROPERTY_OVERRIDE, fakie);
invocation.addPreResultListener(new PreResultListener() {
public void beforeResult(ActionInvocation invocation, String resultCode) {
Map<Object, Object> fakie = (Map<Object, Object>) invocation.getInvocationContext().get(ORIGINAL_PROPERTY_OVERRIDE);
if (fakie != null) {
invocation.getStack().setExprOverrides(fakie);//参数值"xianzhi"存入Map
}
}
});
}
return invocation.invoke();
}
//OgnlValueStack#setExprOverrides
**
* @see com.opensymphony.xwork2.util.ValueStack#setExprOverrides(java.util.Map)
*/
public void setExprOverrides(Map<Object, Object> overrides) {
if (this.overrides == null) {
this.overrides = overrides;
} else {
this.overrides.putAll(overrides);//参数值"xianzhi"存入Map(overrides)中
}
}
3)Action执行完之后渲染页面(这里是productInfo.jsp),页面的${id}怎么解析呢?通过ognl一番折腾,进入下面的方法。
//OgnlValueStack
private Object tryFindValue(String expr) throws OgnlException {
Object value;
expr = lookupForOverrides(expr);
if (defaultType != null) {
value = findValue(expr, defaultType);
} else {
value = getValueUsingOgnl(expr);
if (value == null) {
value = findInContext(expr);
}
}
return value;
}
private String lookupForOverrides(String expr) {
if ((overrides != null) && overrides.containsKey(expr)) {
expr = (String) overrides.get(expr);//overrides是不是有点眼熟?对了,就是上面存“xiaozhi”的Map
}
return expr;
}
如果将struts-product.xml简化为如下:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 2.5//EN"
"http://struts.apache.org/dtds/struts-2.5.dtd">
<struts>
<package name="product" extends="struts-default" namespace="/">
<action name="productInfo" class="demo.action.ProductAction">
<result name="success">/WEB-INF/pages/jsp/productInfo.jsp</result>
</action>
</package>
</struts>
浏览器访问/productInfo?id=xianzhi
咦,404了!
如果再将struts-product.xml修改为如下:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 2.5//EN"
"http://struts.apache.org/dtds/struts-2.5.dtd">
<struts>
<package name="product" extends="struts-default" namespace="/">
<action name="productInfo" class="demo.action.ProductAction">
<result name="input">/WEB-INF/pages/jsp/productInfo.jsp</result>
</action>
</package>
</struts>
咦,又回来了!
看404报错信息“ No result defined for action xxx and result input”,为什么result是input呢?我们最初只定义了success!
原来是拦截器com.opensymphony.xwork2.interceptor.DefaultWorkflowInterceptor改变了result:
//DefaultWorkflowInterceptor#doIntercept
* Intercept {@link ActionInvocation} and returns a <code>inputResultName</code>
* when action / field errors is found registered.
*
* @return String result name
*/
@Override
protected String doIntercept(ActionInvocation invocation) throws Exception {
Object action = invocation.getAction();
if (action instanceof ValidationAware) {
ValidationAware validationAwareAction = (ValidationAware) action;
if (validationAwareAction.hasErrors()) {
if (LOG.isDebugEnabled()) {
LOG.debug("Errors on action [#0], returning result name [#1]", validationAwareAction, inputResultName);
}
String resultName = inputResultName; //inputResultName 默认是"input"
resultName = processValidationWorkflowAware(action, resultName);
resultName = processInputConfig(action, invocation.getProxy().getMethod(), resultName);
resultName = processValidationErrorAware(action, resultName);
return resultName;
}
}
return invocation.invoke();
}
回头看我们的Demo配置,并没有DefaultWorkflowInterceptor,但是在struts-default package中定义了
<interceptor-stack name="defaultStack">
<interceptor-ref name="exception"/>
<interceptor-ref name="alias"/>
<interceptor-ref name="servletConfig"/>
<interceptor-ref name="i18n"/>
<interceptor-ref name="prepare"/>
<interceptor-ref name="chain"/>
<interceptor-ref name="scopedModelDriven"/>
<interceptor-ref name="modelDriven"/>
<interceptor-ref name="fileUpload"/>
<interceptor-ref name="checkbox"/>
<interceptor-ref name="datetime"/>
<interceptor-ref name="multiselect"/>
<interceptor-ref name="staticParams"/>
<interceptor-ref name="actionMappingParams"/>
<interceptor-ref name="params"/>
<interceptor-ref name="conversionError"/>
<interceptor-ref name="validation">
<param name="excludeMethods">input,back,cancel,browse</param>
</interceptor-ref>
<interceptor-ref name="workflow"> <!--就是它!-->
<param name="excludeMethods">input,back,cancel,browse</param>
</interceptor-ref>
<interceptor-ref name="debugging"/>
<interceptor-ref name="deprecation"/>
</interceptor-stack>
1)自定义拦截器配置
2)定义了input “resultName”
笔者曾经跟官方讨论过是否应该杜绝这种坑,但是断断续续两个月的邮件通信后,结论是这应该交给开发者去处理。