你的位置:首页 > Java教程

[Java教程]XSS 防御方法总结


1. XSS攻击原理

XSS原称为CSS(Cross-Site Scripting),因为和层叠样式表(Cascading Style Sheets)重名,所以改称为XSS(X有未知的含义,还有扩展的含义)。XSS攻击涉及到三方:攻击者,用户,web server。用户通过浏览器来访问web server上的网页,XSS攻击就是攻击者通过各种办法,在用户访问的网页中插入自己的脚本,让其在用户访问网页时在浏览器中进行执行。攻击者通过插入的脚本的执行,来获得用户的信息,比如cookie,发送到攻击者自己的网站。所以称为跨站脚本攻击。XSS可以分为反射型XSS和持久性XSS。(一句话,XSS就是在用户的浏览器中执行攻击者自己定制的脚本。)

1.1 反射型XSS

反射性XSS,也就是非持久性XSS。用户点击攻击链接,服务器解析后响应,在返回的响应内容中出现攻击者的XSS代码,被浏览器执行。一来一去,XSS攻击脚本被web server反射回来给浏览器执行,所以称为反射型XSS。

特点:

1> XSS攻击代码非持久性,也就是没有保存在web server中,而是出现在URL地址中;

2> 非持久性,那么攻击方式就不同了。一般是攻击者通过邮件,聊天软件等等方式发送攻击URL,然后用户点击来达到攻击的;

1.2 持久型XSS

区别就是XSS恶意代码存储在web server中,这样,每一个访问特定网页的用户,都会被攻击。

特点:

1> XSS攻击代码存储于web server上;

2> 攻击者,一般是通过网站的留言、评论、博客、日志等等功能(所有能够向web server输入内容的地方),将攻击代码存储到web server上的;

2. XSS 存在的原因

XSS 存在的根本原因是,对URL中的参数,对用户输入提交给web server的内容,没有进行充分的过滤。如果我们能够在web程序中,对用户提交的URL中的参数,和提交的所有内容,进行充分的过滤,将所有的不合法的参数和输入内容过滤掉,那么就不会导致“在用户的浏览器中执行攻击者自己定制的脚本”。

但是,其实充分而完全的过滤,实际上是无法实现的。因为攻击者有各种各样的神奇的,你完全想象不到的方式来绕过服务器端的过滤,最典型的就是对URL和参数进行各种的编码,比如escape, encodeURI, encodeURIComponent, 16进制,10进制,来绕过XSS过滤。那么我们如何来防御XSS呢?

3. XSS 攻击的防御

XSS防御的总体思路是:对输入(和URL参数)进行过滤,对输出进行编码

也就是对提交的所有内容进行过滤,对url中的参数进行过滤,过滤掉会导致脚本执行的相关内容;然后对动态输出到页面的内容进行html编码,使脚本无法在浏览器中执行。虽然对输入过滤可以被绕过,但是也还是会拦截很大一部分的XSS攻击。

3.1 对输入和URL参数进行过滤(白名单和黑名单)

下面贴出一个常用的XSS filter的实现代码:

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

public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper {  HttpServletRequest orgRequest = null;  public XssHttpServletRequestWrapper(HttpServletRequest request) {    super(request);    orgRequest = request;  }  /**   * 覆盖getParameter方法,将参数名和参数值都做xss过滤。<br/>   * 如果需要获得原始的值,则通过super.getParameterValues(name)来获取<br/>   * getParameterNames,getParameterValues和getParameterMap也可能需要覆盖   */  @Override  public String getParameter(String name) {    String value = super.getParameter(xssEncode(name));    if (value != null) {      value = xssEncode(value);    }    return value;  }  /**   * 覆盖getHeader方法,将参数名和参数值都做xss过滤。<br/>   * 如果需要获得原始的值,则通过super.getHeaders(name)来获取<br/>   * getHeaderNames 也可能需要覆盖   */  @Override  public String getHeader(String name) {    String value = super.getHeader(xssEncode(name));    if (value != null) {      value = xssEncode(value);    }    return value;  }  /**   * 将容易引起xss漏洞的半角字符直接替换成全角字符   *   * @param s   * @return   */  private static String xssEncode(String s) {    if (s == null || s.isEmpty()) {      return s;    }    StringBuilder sb = new StringBuilder(s.length() + 16);    for (int i = 0; i < s.length(); i++) {      char c = s.charAt(i);      switch (c) {      case '>':        sb.append('>');// 全角大于号        break;      case '<':        sb.append('<');// 全角小于号        break;      case '\'':        sb.append('‘');// 全角单引号        break;      case '\"':        sb.append('“');// 全角双引号        break;      case '&':        sb.append('&');// 全角        break;      case '\\':        sb.append('\');// 全角斜线        break;      case '#':        sb.append('#');// 全角井号        break;      case '%':  // < 字符的 URL 编码形式表示的 ASCII 字符(十六进制格式) 是: %3c        processUrlEncoder(sb, s, i);        break;      default:        sb.append(c);        break;      }    }    return sb.toString();  }  public static void processUrlEncoder(StringBuilder sb, String s, int index){    if(s.length() >= index + 2){      if(s.charAt(index+1) == '3' && (s.charAt(index+2) == 'c' || s.charAt(index+2) == 'C')){  // %3c, %3C        sb.append('<');        return;      }      if(s.charAt(index+1) == '6' && s.charAt(index+2) == '0'){  // %3c (0x3c=60)        sb.append('<');        return;      }            if(s.charAt(index+1) == '3' && (s.charAt(index+2) == 'e' || s.charAt(index+2) == 'E')){  // %3e, %3E        sb.append('>');        return;      }      if(s.charAt(index+1) == '6' && s.charAt(index+2) == '2'){  // %3e (0x3e=62)        sb.append('>');        return;      }    }    sb.append(s.charAt(index));  }  /**   * 获取最原始的request   *   * @return   */  public HttpServletRequest getOrgRequest() {    return orgRequest;  }  /**   * 获取最原始的request的静态方法   *   * @return   */  public static HttpServletRequest getOrgRequest(HttpServletRequest req) {    if (req instanceof XssHttpServletRequestWrapper) {      return ((XssHttpServletRequestWrapper) req).getOrgRequest();    }    return req;  }}

然后在web.

  <filter>    <filter-name>xssFilter</filter-name>    <filter-class>com.xxxxxx.filter.XssFilter</filter-class>  </filter>  <filter-mapping>    <filter-name>xssFilter</filter-name>    <url-pattern>/*</url-pattern>  </filter-mapping>

主要的思路就是将容易导致XSS攻击的边角字符替换成全角字符。< 和 > 是脚本执行和各种html标签需要的,比如 <script>,& 和 # 以及 % 在对URL编码试图绕过XSS filter时,会出现。我们说对输入的过滤分为白名单和黑名单。上面的XSS filter就是一种黑名单的过滤,黑名单就是列出不能出现的对象的清单,一旦出现就进行处理。还有一种白名单的过滤,白名单就是列出可被接受的内容,比如规定所有的输入只能是“大小写的26个英文字母和10个数字,还有-和_”,所有其他的输入都是非法的,会被抛弃掉。很显然如此严格的白名单是可以100%拦截所有的XSS攻击的。但是现实情况一般是不能进行如此严格的白名单过滤的。

对于输入,处理使用XSS filter之外,对于每一个输入,在客户端和服务器端还要进行各种验证,验证是否合法字符,长度是否合法,格式是否正确。在客户端和服务端都要进行验证,因为客户端的验证很容易被绕过。其实这种验证也分为了黑名单和白名单。黑名单的验证就是不能出现某些字符,白名单的验证就是只能出现某些字符。尽量使用白名单。

3.2 对输出进行编码

在输出数据之前对潜在的威胁的字符进行编码、转义是防御XSS攻击十分有效的措施。如果使用好的话,理论上是可以防御住所有的XSS攻击的。

对所有要动态输出到页面的内容,通通进行相关的编码和转义。当然转义是按照其输出的上下文环境来决定如何转义的。

1> 作为body文本输出,作为html标签的属性输出:

比如:<span>${username}</span>, <p><c:out value="${username}"></c:out></p>

<input type="text" value="${username}" />

此时的转义规则如下:

< 转成 &lt;

> 转成 &gt;

& 转成 &amp;

" 转成 &quot;

' 转成 &#39

2> javascript事件

<input type="button" onclick='go_to_url("${myUrl}");' />

除了上面的那些转义之外,还要附加上下面的转义:

\ 转成 \\

/ 转成 \/

; 转成 ;(全角;)

3> URL属性

如果 <script>, <style>, <imt> 等标签的 src 和 href 属性值为动态内容,那么要确保这些url没有执行恶意连接。

确保:href 和 src 的值必须以 http://开头,白名单方式;不能有10进制和16进制编码字符。

4. 总结下

XSS攻击访问方法:对输入(和URL参数)进行过滤,对输出进行编码;白名单和黑名单结合;