项目中有好多通过EL表达式输出字符时没有考虑XSS问题,比如
<div>
<span>${user.name}span>
div>
简单来说,可以使用jstl标签来显示就好了:
<div>
<span><c:out value="${user.name}"/>span>
div>
问题是,系统中非常多地方都存在这种问题,要一一修改工作量简直就是灾难啊。
我想到的办法是,改变EL表达式的输出行为,使EL表达式输出的字符串本身就已经是替换过HTML敏感字符的了。
翻Tomcat的源码,最终找到了如下代码(org.apache.jasper.el.JasperELResolver):
@Override
public Object getValue(ELContext context, Object base, Object property)
throws NullPointerException, PropertyNotFoundException, ELException {
context.setPropertyResolved(false);
int start;
Object result = null;
if (base == null) {
// call implicit and app resolvers
int index = 1 /* implicit */ + appResolversSize;
for (int i = 0; i < index; i++) {
result = resolvers[i].getValue(context, base, property);
if (context.isPropertyResolved()) {
return result;
}
}
// skip stream, static and collection-based resolvers (map,
// resource, list, array) and bean
start = index + 7;
} else {
// skip implicit resolver only
start = 1;
}
for (int i = start; i < size; i++) {
result = resolvers[i].getValue(context, base, property);
if (context.isPropertyResolved()) {
return result;
}
}
return null;
}
这里遍历几个内置的ELResolver来进行表达式计算,几个Resolver包括:
可以确定,这里就是EL表达式最终返回到JSP的地方了。我需要在这里在返回之前,对result进行一些处理。
怎么实现符合XSS的字符,大可不必自己来写,jstl的out标签早就已经为我们写好了。下载jstl-impl源码,找到OutSupport.java的源码,可以看到如下代码:
/**
* Outputs text to pageContext's current JspWriter.
* If escapeXml is true, performs the following substring
* replacements (to facilitate output to XML/HTML pages):
*
* & -> &
* < -> <
* > -> >
* " -> "
* ' -> '
*
* See also Util.escapeXml().
*/
public static void out(PageContext pageContext,
boolean escapeXml,
Object obj) throws IOException {
JspWriter w = pageContext.getOut();
if (!escapeXml) {
// write chars as is
if (obj instanceof Reader) {
Reader reader = (Reader)obj;
char[] buf = new char[4096];
int count;
while ((count=reader.read(buf, 0, 4096)) != -1) {
w.write(buf, 0, count);
}
} else {
w.write(obj.toString());
}
} else {
// escape XML chars
if (obj instanceof Reader) {
Reader reader = (Reader)obj;
char[] buf = new char[4096];
int count;
while ((count = reader.read(buf, 0, 4096)) != -1) {
writeEscapedXml(buf, count, w);
}
} else {
String text = obj.toString();
writeEscapedXml(text.toCharArray(), text.length(), w);
}
}
}
咱可以把这段代码移动到JasperELResolver.java中,然后创建下面这个方法:
private Object ifStringThenHTMLSecurity(Object value){
if(value instanceof String){
String str = (String)value;
str = escapeXml(str);
}
return value;
}
然后在JasperELResolver.java中的getValue方法中就可以直接对result进行调用了:
@Override
public Object getValue(ELContext context, Object base, Object property)
throws NullPointerException, PropertyNotFoundException, ELException {
context.setPropertyResolved(false);
int start;
Object result = null;
if (base == null) {
// call implicit and app resolvers
int index = 1 /* implicit */ + appResolversSize;
for (int i = 0; i < index; i++) {
result = resolvers[i].getValue(context, base, property);
if (context.isPropertyResolved()) {
return ifStringThenHTMLSecurity(result);
}
}
// skip stream, static and collection-based resolvers (map,
// resource, list, array) and bean
start = index + 7;
} else {
// skip implicit resolver only
start = 1;
}
for (int i = start; i < size; i++) {
result = resolvers[i].getValue(context, base, property);
if (context.isPropertyResolved()) {
return ifStringThenHTMLSecurity(result);
}
}
return null;
}
这样,就顺利的将EL表达式输出的字符串直接拥有了jstl的out标签特性了。
随便提一下修改Tomcat源码的方法。修改Tomcat源码时,不能像修改Spring之类的框架的源码那样,将同名(全限定名)放在项目src下就行了,而是需要对Tomcat的lib目录下的jar文件进行处理。我这里是需要修改JasperELResolver.java,找到源码之后,将它移动到自己的项目中:
这样,在编译成功之后,找到JasperELResolver.class文件,用它他替换Tomcat的lib/jasper.jar文件中对应的同名class文件 。
最后不得不说的是,这种方式的缺点是,对项目整体有些影响,因为EL表达式本身输出的内容已经拥有了原先需要通过out标签输出才有的特性了。所以项目中已经在使用out标签输出的需要改为直接输出。另外,真正有地方需要输出HTML时,需要手动再将HTML转义字符给替换回来。