通过修改EL表达式输出行为解决XSS问题

项目中有好多通过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表达式输出行为解决XSS问题_第1张图片

可以确定,这里就是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转义字符给替换回来。

你可能感兴趣的:(Java)