V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
liugp5201314
V2EX  ›  Java

SpringMVC 增加了一个 xss 过滤器,导致 Controller 上传的文件为空

  •  2
     
  •   liugp5201314 · 2020-05-07 14:22:09 +08:00 · 2763 次点击
    这是一个创建于 1421 天前的主题,其中的信息可能已经有所发展或是发生改变。

    最近做的一个项目进行安全测试时测出了 SQL 注入问题,严重级别为高危,怎么办呢?我还是个雏,还没学会飞呢,挠挠头,硬上吧,然后把之前项目里的 xss 都弄过来修修改改,然后跑起来,震惊了,竟然全都过滤了,是的,全都过滤了,连上传的文件都给我过滤了,咋办?再百度,结果全是千篇一律的抄袭,没一个能用的,还是发个帖子大家帮我瞅瞅,看看怎么解决一下,头发都挠掉一大把了,听说植发一根二十块,听着都吓人。

    这是调用过滤器:

    
    ```java
     public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
        throws IOException, ServletException{
    HttpServletRequest httpRequest = (HttpServletRequest) request;
    HttpServletResponse httpResponse = (HttpServletResponse) response;
    String enctype = request.getContentType();
    if (StringUtils.isNotBlank(enctype) && enctype.contains("multipart/form-data")) {
       final MultipartResolver multipartResolver = SpringUtil.getBean("multipartResolver");
       final MultipartHttpServletRequest multipartHttpServletRequest = 	  	   multipartResolver.resolveMultipart((HttpServletRequest) request);
       chain.doFilter(new XssHttpServletRequestWrapper(multipartHttpServletRequest),   response);
    } else {
         chain.doFilter(new XssHttpServletRequestWrapper((HttpServletRequest) request), response);
    }
    }
    
    
    这是重写的方法:
    
    ```java
    public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper
    {
        /**
         * @param request
         */
        public XssHttpServletRequestWrapper(HttpServletRequest request)
        {
            super(request);
        }
    
        /**
         * 覆盖 getHeader 方法,将参数名和参数值都做 xss 过滤。
         * 如果需要获得原始的值,则通过 super.getHeaders(name)来获取
         * getHeaderNames 也可能需要覆盖
         */
        @Override
        public String getHeader(String name) {
    
            String value = super.getHeader(EscapeUtil.escape(name));
            if (value != null) {
                value = EscapeUtil.escape(value);
            }
            return value;
        }
    
        @Override
        public String getParameter(String name){
            String value = super.getParameter(name);
            if (value != null)
            {
            String escapseValue = EscapeUtil.escape(value.trim());
            return escapseValue;
            }
            return super.getParameter(name);
        }
    
        @Override
        public String[] getParameterValues(String name)
        {
            String[] values = super.getParameterValues(name);
            if (values != null)
            {
                int length = values.length;
                String[] escapseValues = new String[length];
                for (int i = 0; i < length; i++)
                {
                    // 防 xss 攻击和过滤前后空格
                    escapseValues[i] = EscapeUtil.escape(values[i]).trim();
                }
                return escapseValues;
            }
            return super.getParameterValues(name);
        }
    
    }
    

    这是过滤规则:

    public class EscapeUtil
    {
        public static final String RE_HTML_MARK = "(<[^<]*?>)|(<[\\s]*?/[^<]*?>)|(<[^<]*?/[\\s]*?>)";
    
        private static final char[][] TEXT = new char[64][];
    
        static
        {
            for (int i = 0; i < 64; i++)
            {
                TEXT[i] = new char[] { (char) i };
            }
    
            // special HTML characters
            TEXT['\''] = "&#039;".toCharArray(); // 单引号
            TEXT['"'] = "&#34;".toCharArray(); // 单引号
            TEXT['&'] = "&#38;".toCharArray(); // &符
            TEXT['<'] = "&#60;".toCharArray(); // 小于号
            TEXT['>'] = "&#62;".toCharArray(); // 大于号
        }
    
        /**
         * 转义文本中的 HTML 字符为安全的字符
         * 
         * @param text 被转义的文本
         * @return 转义后的文本
         */
        public static String escape(String text)
        {
            return encode(text);
        }
    
        /**
         * 还原被转义的 HTML 特殊字符
         * 
         * @param content 包含转义符的 HTML 内容
         * @return 转换后的字符串
         */
        public static String unescape(String content)
        {
            return decode(content);
        }
    
        /**
         * 清除所有 HTML 标签,但是不删除标签内的内容
         * 
         * @param content 文本
         * @return 清除标签后的文本
         */
        public static String clean(String content)
        {
            return new HTMLFilter().filter(content);
        }
    
        /**
         * Escape 编码
         * 
         * @param text 被编码的文本
         * @return 编码后的字符
         */
        private static String encode(String text)
        {
            int len;
            if ((text == null) || ((len = text.length()) == 0))
            {
                return StringUtils.EMPTY;
            }
            StringBuilder buffer = new StringBuilder(len + (len >> 2));
            char c;
            for (int i = 0; i < len; i++)
            {
                c = text.charAt(i);
                if (c < 64)
                {
                    buffer.append(TEXT[c]);
                }
                else
                {
                    buffer.append(c);
                }
            }
            return buffer.toString();
        }
    
        /**
         * Escape 解码
         * 
         * @param content 被转义的内容
         * @return 解码后的字符串
         */
        public static String decode(String content)
        {
            if (StringUtils.isEmpty(content))
            {
                return content;
            }
    
            StringBuilder tmp = new StringBuilder(content.length());
            int lastPos = 0, pos = 0;
            char ch;
            while (lastPos < content.length())
            {
                pos = content.indexOf("%", lastPos);
                if (pos == lastPos)
                {
                    if (content.charAt(pos + 1) == 'u')
                    {
                        ch = (char) Integer.parseInt(content.substring(pos + 2, pos + 6), 16);
                        tmp.append(ch);
                        lastPos = pos + 6;
                    }
                    else
                    {
                        ch = (char) Integer.parseInt(content.substring(pos + 1, pos + 3), 16);
                        tmp.append(ch);
                        lastPos = pos + 3;
                    }
                }
                else
                {
                    if (pos == -1)
                    {
                        tmp.append(content.substring(lastPos));
                        lastPos = content.length();
                    }
                    else
                    {
                        tmp.append(content.substring(lastPos, pos));
                        lastPos = pos;
                    }
                }
            }
            return tmp.toString();
        }
    
        public static void main(String[] args)
        {
            String html = "<script>alert(1);</script>";
            // String html = "<scr<script>ipt>alert(\"XSS\")</scr<script>ipt>";
            // String html = "<123";
            System.out.println(EscapeUtil.clean(html));
            System.out.println(EscapeUtil.escape(html));
            System.out.println(EscapeUtil.unescape(html));
        }
    }
    
    16 条回复    2020-05-16 14:58:29 +08:00
    waa
        1
    waa  
       2020-05-07 17:02:00 +08:00
    在 controller 方法参数中直接添加 MultipartHttpServletRequest 形式参数,通过 multipartHttpservletRequest 对象获取 MultipartFile 和其余的请求参数。我的是这样解决的
    jzmws
        2
    jzmws  
       2020-05-07 20:11:42 +08:00
    forty 测试的 ??
    FreeEx
        3
    FreeEx  
       2020-05-07 20:48:59 +08:00
    1. sql 注入你加 xss 过滤器有啥作用?
    2. 判断 ContentType 之后的处理有问题。
    sagaxu
        4
    sagaxu  
       2020-05-07 21:00:14 +08:00 via Android
    你这个思路很 php
    richard1122
        5
    richard1122  
       2020-05-07 21:14:00 +08:00
    想起了无数年前 PHP 开的 magic quote
    chendy
        6
    chendy  
       2020-05-08 07:59:33 +08:00
    建议直接写 servlet 算了,这 springmvc 用的不如不用
    liugp5201314
        7
    liugp5201314  
    OP
       2020-05-08 10:33:44 +08:00
    @FreeEx 第一次弄,我也是百度的,说 xss 是防 sql 注入的
    liugp5201314
        8
    liugp5201314  
    OP
       2020-05-08 10:35:11 +08:00
    @waa 是吧原来 controller 里的 HttpServletRequest 这个参数替换为 MultipartHttpServletRequest 这个吗
    MrMario
        9
    MrMario  
       2020-05-08 14:42:09 +08:00
    注入问题根源是 sql 的处理,预编译+参数绑定,或者使用 ORM 框架就好(个别框架使用需注意)
    liugp5201314
        10
    liugp5201314  
    OP
       2020-05-09 10:30:21 +08:00
    @MrMario 框架已经订好了,这是个维护的项目,我只能改功能,不能改框架
    ice2neet
        11
    ice2neet  
       2020-05-09 13:43:24 +08:00
    sql 注入不是应该处理 sql 吗?
    BryceL
        12
    BryceL  
       2020-05-12 11:17:53 +08:00
    写个 AOP 对入参进行处理。你这 sql 注入是指的参数的问题把。
    xinQing
        13
    xinQing  
       2020-05-13 18:01:53 +08:00
    哈哈,我遇到过类似的问题。搞了个过滤器过滤请求内容,然后 controller 里面的数据拿不到了。这是因为正常情况下流只能处理一次,你过滤器消费了,后续就没有了。你要采用 warpper 包装 Request,让 Request 支持可重复消费。spring 可以用这个包装下 org.springframework.web.util.ContentCachingRequestWrapper
    liugp5201314
        14
    liugp5201314  
    OP
       2020-05-14 10:14:08 +08:00
    @xinQing 你看我上边的代码。我已经重写了 warpper 了。但还是不行,不知是不是哪里写的不对
    xinQing
        15
    xinQing  
       2020-05-15 09:27:54 +08:00
    @liugp5201314 说了啊,你写的有问题,你看看 org.springframework.web.util.ContentCachingRequestWrapper 用 ByteArrayInputStream 缓存数据,使流支持可重复读取
    340244120w
        16
    340244120w  
       2020-05-16 14:58:29 +08:00
    上传的接口 直接 chain.doFilter(request, response)就行了。。不用包装
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   5504 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 35ms · UTC 08:19 · PVG 16:19 · LAX 01:19 · JFK 04:19
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.