我百度搜索了一下,跨站脚本攻击,相关内容超过 500 多万,但是相比 NPE 来说,显然还不够。很多程序员不重视跨站脚本攻击,导致很多开源项目都存在这样的漏洞。


今天我们基于 SpringBoot 来实现一个跨站脚本过滤器,彻底的搞定跨站脚本攻击!


<script>alert('hello,gaga!');</script>>"'><img src="javascript.:alert('XSS')">>"'><script>alert('XSS')</script><table background='javascript.:alert(([code])'></table><object type=text/html data='javascript.:alert(([code]);'></object>"+alert('XSS')+"'><script>alert(document.cookie)</script>='><script>alert(document.cookie)</script><script>alert(document.cookie)</script><script>alert(vulnerable)</script><s&#99;ript>alert('XSS')</script><img src="javas&#99;ript:alert('XSS')">%0a%0a<script>alert(\"Vulnerable\")</script>.jsp%3c/a%3e%3cscript%3ealert(%22xss%22)%3c/script%3e%3c/title%3e%3cscript%3ealert(%22xss%22)%3c/script%3e%3cscript%3ealert(%22xss%22)%3c/script%3e/index.html<script>alert('Vulnerable')</script>a.jsp/<script>alert('Vulnerable')</script>"><script>alert('Vulnerable')</script><IMG SRC="javascript.:alert('XSS');"><IMG src="/javascript.:alert"('XSS')><IMG src="/JaVaScRiPt.:alert"('XSS')><IMG src="/JaVaScRiPt.:alert"(&quot;XSS&quot;)><IMG SRC="jav&#x09;ascript.:alert('XSS');"><IMG SRC="jav&#x0A;ascript.:alert('XSS');"><IMG SRC="jav&#x0D;ascript.:alert('XSS');">"<IMG src="/java"\0script.:alert(\"XSS\")>";'>out<IMG SRC=" javascript.:alert('XSS');"><SCRIPT>a=/XSS/alert(a.source)</SCRIPT><BODY BACKGROUND="javascript.:alert('XSS')"><BODY ONLOAD=alert('XSS')><IMG DYNSRC="javascript.:alert('XSS')"><IMG LOWSRC="javascript.:alert('XSS')"><BGSOUND SRC="javascript.:alert('XSS');"><br size="&{alert('XSS')}"><LAYER SRC="http://xss.ha.ckers.org/a.js"></layer><LINK REL="stylesheet"HREF="javascript.:alert('XSS');"><IMG SRC='vbscript.:msgbox("XSS")'><META. HTTP-EQUIV="refresh"CONTENT="0;url=javascript.:alert('XSS');"><IFRAME. src="/javascript.:alert"('XSS')></IFRAME><FRAMESET><FRAME. src="/javascript.:alert"('XSS')></FRAME></FRAMESET><TABLE BACKGROUND="javascript.:alert('XSS')"><DIV STYLE="background-image: url(javascript.:alert('XSS'))"><DIV STYLE="behaviour: url('http://www.how-to-hack.org/exploit.html&#39;);"><DIV STYLE="width: expression(alert('XSS'));"><STYLE>@im\port'\ja\vasc\ript:alert("XSS")';</STYLE><IMG STYLE='xss:expre\ssion(alert("XSS"))'><STYLE. TYPE="text/javascript">alert('XSS');</STYLE><STYLE. TYPE="text/css">.XSS{background-image:url("javascript.:alert('XSS')");}</STYLE><A CLASS=XSS></A><STYLE. type="text/css">BODY{background:url("javascript.:alert('XSS')")}</STYLE><BASE HREF="javascript.:alert('XSS');//">getURL("javascript.:alert('XSS')")a="get";b="URL";c=

上面的内容就是我们常见跨站攻击脚本,有些安全企业基于此制作了在线的跨站攻击漏洞检测工具。


废话不多说了,我们直接开始动手吧!


先实现一个过滤器。

public class XssFilter implements Filter {    @Override    public void init(FilterConfig config) throws ServletException {}    @Override    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)            throws IOException, ServletException {        XssHttpServletRequestWrapper xssHttpServletRequestWrapper = new XssHttpServletRequestWrapper((HttpServletRequest)request);        chain.doFilter(xssHttpServletRequestWrapper, response);    }    @Override    public void destroy() {}}


再实现一个敏感字符转换类。


  1. public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper {

  2.    HttpServletRequest orgRequest = null;


  3.    private String body;


  4.    public XssHttpServletRequestWrapper(HttpServletRequest request) {

  5.        super(request);

  6.        orgRequest = request;

  7.        body = HttpGetBody.getBodyString(request);

  8.    }


  9.    /**

  10.     * 覆盖getParameter方法,将参数名和参数值都做xss过滤。<br/>

  11.     * 如果需要获得原始的值,则通过super.getParameterValues(name)来获取<br/>

  12.     * getParameterNames,getParameterValues和getParameterMap也可能需要覆盖

  13.     */

  14.    @Override

  15.    public String getParameter(String name) {

  16.        String value = super.getParameter(xssEncode(name, 0));

  17.        if (null != value) {

  18.            value = xssEncode(value, 0);

  19.        }

  20.        return value;

  21.    }


  22.    @Override

  23.    public String[] getParameterValues(String name) {

  24.        String[] values = super.getParameterValues(xssEncode(name, 0));

  25.        if (values == null) {

  26.            return null;

  27.        }

  28.        int count = values.length;

  29.        String[] encodedValues = new String[count];

  30.        for (int i = 0; i < count; i++) {

  31.            encodedValues[i] = xssEncode(values[i], 0);

  32.        }

  33.        return encodedValues;

  34.    }


  35.    @Override

  36.    public Map getParameterMap() {

  37.        HashMap paramMap = (HashMap) super.getParameterMap();

  38.        paramMap = (HashMap) paramMap.clone();

  39.        for (Iterator iterator = paramMap.entrySet().iterator(); iterator.hasNext(); ) {

  40.            Map.Entry entry = (Map.Entry) iterator.next();

  41.            String[] values = (String[]) entry.getValue();

  42.            for (int i = 0; i < values.length; i++) {

  43.                if (values[i] instanceof String) {

  44.                    values[i] = xssEncode(values[i], 0);

  45.                }

  46.            }

  47.            entry.setValue(values);

  48.        }

  49.        return paramMap;

  50.    }


  51.    @Override

  52.    public ServletInputStream getInputStream() throws IOException {

  53.        ServletInputStream inputStream = null;

  54.        if (StringUtil.isNotEmpty(body)) {

  55.            body = xssEncode(body, 1);

  56.            inputStream = new TranslateServletInputStream(body);

  57.        }

  58.        return inputStream;

  59.    }


  60.    /**

  61.     * 覆盖getHeader方法,将参数名和参数值都做xss过滤。<br/>

  62.     * 如果需要获得原始的值,则通过super.getHeaders(name)来获取<br/>

  63.     * getHeaderNames 也可能需要覆盖

  64.     */

  65.    @Override

  66.    public String getHeader(String name) {

  67.        String value = super.getHeader(xssEncode(name, 0));

  68.        if (value != null) {

  69.            value = xssEncode(value, 0);

  70.        }

  71.        return value;

  72.    }


  73.    /**

  74.     * 将容易引起xss漏洞的半角字符直接替换成全角字符

  75.     * @param s

  76.     * @return

  77.     */

  78.    private static String xssEncode(String s, int type) {

  79.        if (s == null || s.isEmpty()) {

  80.            return s;

  81.        }

  82.        StringBuilder sb = new StringBuilder(s.length() + 16);

  83.        for (int i = 0; i < s.length(); i++) {

  84.            char c = s.charAt(i);

  85.            if (type == 0) {

  86.                switch (c) {

  87.                    case '\'':

  88.                        // 全角单引号

  89.                        sb.append('‘');

  90.                        break;

  91.                    case '\"':

  92.                        // 全角双引号

  93.                        sb.append('“');

  94.                        break;

  95.                    case '>':

  96.                        // 全角大于号

  97.                        sb.append('>');

  98.                        break;

  99.                    case '<':

  100.                        // 全角小于号

  101.                        sb.append('<');

  102.                        break;

  103.                    case '&':

  104.                        // 全角&符号

  105.                        sb.append('&');

  106.                        break;

  107.                    case '\\':

  108.                        // 全角斜线

  109.                        sb.append('\');

  110.                        break;

  111.                    case '#':

  112.                        // 全角井号

  113.                        sb.append('#');

  114.                        break;

  115.                    // < 字符的 URL 编码形式表示的 ASCII 字符(十六进制格式) 是: %3c

  116.                    case '%':

  117.                        processUrlEncoder(sb, s, i);

  118.                        break;

  119.                    default:

  120.                        sb.append(c);

  121.                        break;

  122.                }

  123.            } else {

  124.                switch (c) {

  125.                    case '>':

  126.                        // 全角大于号

  127.                        sb.append('>');

  128.                        break;

  129.                    case '<':

  130.                        // 全角小于号

  131.                        sb.append('<');

  132.                        break;

  133.                    case '&':

  134.                        // 全角&符号

  135.                        sb.append('&');

  136.                        break;

  137.                    case '\\':

  138.                        // 全角斜线

  139.                        sb.append('\');

  140.                        break;

  141.                    case '#':

  142.                        // 全角井号

  143.                        sb.append('#');

  144.                        break;

  145.                    // < 字符的 URL 编码形式表示的 ASCII 字符(十六进制格式) 是: %3c

  146.                    case '%':

  147.                        processUrlEncoder(sb, s, i);

  148.                        break;

  149.                    default:

  150.                        sb.append(c);

  151.                        break;

  152.                }

  153.            }


  154.        }

  155.        return sb.toString();

  156.    }


  157.    public static void processUrlEncoder(StringBuilder sb, String s, int index) {

  158.        if (s.length() >= index + 2) {

  159.            // %3c, %3C

  160.            if (s.charAt(index + 1) == '3' && (s.charAt(index + 2) == 'c' || s.charAt(index + 2) == 'C')) {

  161.                sb.append('<');

  162.                return;

  163.            }

  164.            // %3c (0x3c=60)

  165.            if (s.charAt(index + 1) == '6' && s.charAt(index + 2) == '0') {

  166.                sb.append('<');

  167.                return;

  168.            }

  169.            // %3e, %3E

  170.            if (s.charAt(index + 1) == '3' && (s.charAt(index + 2) == 'e' || s.charAt(index + 2) == 'E')) {

  171.                sb.append('>');

  172.                return;

  173.            }

  174.            // %3e (0x3e=62)

  175.            if (s.charAt(index + 1) == '6' && s.charAt(index + 2) == '2') {

  176.                sb.append('>');

  177.                return;

  178.            }

  179.        }

  180.        sb.append(s.charAt(index));

  181.    }


  182.    /**

  183.     * 获取最原始的request

  184.     * @return

  185.     */

  186.    public HttpServletRequest getOrgRequest() {

  187.        return orgRequest;

  188.    }


  189.    /**

  190.     * 获取最原始的request的静态方法

  191.     * @return

  192.     */

  193.    public static HttpServletRequest getOrgRequest(HttpServletRequest req) {

  194.        if (req instanceof XssHttpServletRequestWrapper) {

  195.            return ((XssHttpServletRequestWrapper) req).getOrgRequest();

  196.        }

  197.        return req;

  198.    }

  199. }


接下来我们要使用这两个类。


  1. @Configuration

  2. public class XSSFilterConfig {

  3.    @Bean

  4.    public FilterRegistrationBean filterRegistrationBean() {

  5.        FilterRegistrationBean registration = new FilterRegistrationBean();

  6.        registration.setFilter(xssFilter());

  7.        registration.addUrlPatterns("/*");

  8.        registration.addInitParameter("paramName", "paramValue");

  9.        registration.setName("xssFilter");

  10.        return registration;

  11.    }


  12.    /**

  13.     * 创建一个bean

  14.     * @return

  15.     */

  16.    @Bean(name = "xssFilter")

  17.    public Filter xssFilter() {

  18.        return new XssFilter();

  19.    }

  20. }


在博客上发表之后,有人说我的类不完整,所以,下面的两个类是上面用到的两个类,贴在这里。


  1. import java.io.ByteArrayInputStream;

  2. import java.io.IOException;

  3. import java.io.InputStream;

  4. import java.nio.charset.Charset;


  5. import javax.servlet.ReadListener;

  6. import javax.servlet.ServletInputStream;



  7. public class TranslateServletInputStream extends ServletInputStream {

  8.    private InputStream inputStream;

  9.    /**

  10.     * 解析json之后的文本

  11.     */

  12.    private String body;


  13.    public TranslateServletInputStream(String body) throws IOException {

  14.        this.body = body;

  15.        inputStream = null;

  16.    }


  17.    @Override

  18.    public boolean isReady() {

  19.        return false;

  20.    }


  21.    @Override

  22.    public void setReadListener(ReadListener readListener) {


  23.    }


  24.    @Override

  25.    public boolean isFinished() {

  26.        return false;

  27.    }


  28.    private InputStream acquireInputStream() throws IOException {

  29.        if (inputStream == null) {

  30.            inputStream = new ByteArrayInputStream(body.getBytes(Charset.forName("UTF-8")));

  31.            //通过解析之后传入的文本生成inputStream以便后面controller调用

  32.        }


  33.        return inputStream;

  34.    }


  35.    @Override

  36.    public void close() throws IOException {

  37.        try {

  38.            if (inputStream != null) {

  39.                inputStream.close();

  40.            }

  41.        } catch (IOException e) {

  42.            throw e;

  43.        } finally {

  44.            inputStream = null;

  45.        }

  46.    }


  47.    @Override

  48.    public boolean markSupported() {

  49.        return false;

  50.    }


  51.    @Override

  52.    public synchronized void mark(int i) {

  53.        throw new UnsupportedOperationException("mark not supported");

  54.    }


  55.    @Override

  56.    public synchronized void reset() throws IOException {

  57.        throw new IOException(new UnsupportedOperationException("reset not supported"));

  58.    }


  59.    @Override

  60.    public int read() throws IOException {

  61.        return acquireInputStream().read();


  62.    }

  63. }


HttpGetBody 代码如下所示:


  1. import javax.servlet.ServletRequest;

  2. import java.io.BufferedReader;

  3. import java.io.IOException;

  4. import java.io.InputStream;

  5. import java.io.InputStreamReader;

  6. import java.nio.charset.Charset;


  7. public class HttpGetBody {


  8.    /**

  9.     * 获取请求Body

  10.     * @param request

  11.     * @return

  12.     */

  13.    public static String getBodyString(ServletRequest request) {

  14.        StringBuffer sb = new StringBuffer();

  15.        InputStream inputStream = null;

  16.        BufferedReader reader = null;

  17.        try {

  18.            inputStream = request.getInputStream();

  19.            reader = new BufferedReader(new InputStreamReader(inputStream, Charset.forName("UTF-8")));

  20.            String line = "";

  21.            while ((line = reader.readLine()) != null) {

  22.                sb.append(line);

  23.            }

  24.        } catch (IOException e) {

  25.            e.printStackTrace();

  26.        } finally {

  27.            if (inputStream != null) {

  28.                try {

  29.                    inputStream.close();

  30.                } catch (IOException e) {

  31.                    e.printStackTrace();

  32.                }

  33.            }

  34.            if (reader != null) {

  35.                try {

  36.                    reader.close();

  37.                } catch (IOException e) {

  38.                    e.printStackTrace();

  39.                }

  40.            }

  41.        }

  42.        return sb.toString();

  43.    }

  44. }

上面的代码都很简单,原理搞懂了之后,实现起来就非常的顺手。


针对上面的代码,大家可以再进行优化。


除此之外,我还将上面的代码制作成了 starter,其他项目只要引入了我的这个 xttblog-xss-starter,即可实现防御跨站脚本攻击!


五一放假之后,我发现群里有几个网友,学习动力十足!很看好他们!可惜我在带娃,没时间和他们一起唠叨!空闲时间,码出了这篇文章,希望能对大家有所帮助!


最后还是希望大家多多学习!毕竟 00 后都奔 20 了。祝大家五四青年节快乐!