XSS 防護方法總結

1. XSS攻擊原理javascript

XSS原稱爲CSS(Cross-Site Scripting),由於和層疊樣式表(Cascading Style Sheets)重名,因此改稱爲XSS(X通常有未知的含義,還有擴展的含義)。XSS攻擊涉及到三方:攻擊者,用戶,web server。用戶是經過瀏覽器來訪問web server上的網頁,XSS攻擊就是攻擊者經過各類辦法,在用戶訪問的網頁中插入本身的腳本,讓其在用戶訪問網頁時在其瀏覽器中進行執行。攻擊者經過插入的腳本的執行,來得到用戶的信息,好比cookie,發送到攻擊者本身的網站(跨站了)。因此稱爲跨站腳本攻擊。XSS能夠分爲反射型XSS和持久性XSS,還有DOM Based XSS。(一句話,XSS就是在用戶的瀏覽器中執行攻擊者本身定製的腳本。)php

1.1 反射型XSShtml

反射性XSS,也就是非持久性XSS。用戶點擊攻擊連接,服務器解析後響應,在返回的響應內容中出現攻擊者的XSS代碼,被瀏覽器執行。一來一去,XSS攻擊腳本被web server反射回來給瀏覽器執行,因此稱爲反射型XSS。java

特色:nginx

1> XSS攻擊代碼非持久性,也就是沒有保存在web server中,而是出如今URL地址中;web

2> 非持久性,那麼攻擊方式就不一樣了。通常是攻擊者經過郵件,聊天軟件等等方式發送攻擊URL,而後用戶點擊來達到攻擊的;ajax

1.2 持久型XSSsql

區別就是XSS惡意代碼存儲在web server中,這樣,每個訪問特定網頁的用戶,都會被攻擊。apache

特色:c#

1> XSS攻擊代碼存儲於web server上;

2> 攻擊者,通常是經過網站的留言、評論、博客、日誌等等功能(全部可以向web server輸入內容的地方),將攻擊代碼存儲到web server上的;

有時持久性XSS和反射型XSS是同時使用的,好比先經過對一個攻擊url進行編碼(來繞過xss filter),而後提交該web server(存儲在web server中), 而後用戶在瀏覽頁面時,若是點擊該url,就會觸發一個XSS攻擊。固然用戶點擊該url時,也可能會觸發一個CSRF(Cross site request forgery)攻擊。

1.3 DOM based XSS

基於DOM的XSS,也就是web server不參與,僅僅涉及到瀏覽器的XSS。好比根據用戶的輸入來動態構造一個DOM節點,若是沒有對用戶的輸入進行過濾,那麼也就致使XSS攻擊的產生。過濾能夠考慮採用esapi4js

參見:http://www.freebuf.com/articles/web/29177.html , http://www.zhihu.com/question/26628342/answer/33504799

2. XSS 存在的緣由

XSS 存在的根本緣由是,對URL中的參數,對用戶輸入提交給web server的內容,沒有進行充分的過濾。若是咱們可以在web程序中,對用戶提交的URL中的參數,和提交的全部內容,進行充分的過濾,將全部的不合法的參數和輸入內容過濾掉,那麼就不會致使「在用戶的瀏覽器中執行攻擊者本身定製的腳本」。

可是,其實充分而徹底的過濾,其實是沒法實現的。由於攻擊者有各類各樣的神奇的,你徹底想象不到的方式來繞過服務器端的過濾,最典型的就是對URL和參數進行各類的編碼,好比escape, encodeURI, encodeURIComponent, 16進制,10進制,8進制,來繞過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.xml中配置該filter:

    <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. HttpOnly 與 XSS防護

XSS 通常利用js腳步讀取用戶瀏覽器中的Cookie,而若是在服務器端對 Cookie 設置了HttpOnly 屬性,那麼js腳本就不能讀取到cookie,可是瀏覽器仍是可以正常使用cookie。(下面的內容轉自:http://www.oschina.net/question/12_72706)

通常的Cookie都是從document對象中得到的,如今瀏覽器在設置 Cookie的時候通常都接受一個叫作HttpOnly的參數,跟domain等其餘參數同樣,一旦這個HttpOnly被設置,你在瀏覽器的 document對象中就看不到Cookie了,而瀏覽器在瀏覽的時候不受任何影響,由於Cookie會被放在瀏覽器頭中發送出去(包括ajax的時 候),應用程序也通常不會在js裏操做這些敏感Cookie的,對於一些敏感的Cookie咱們採用HttpOnly,對於一些須要在應用程序中用js操做的cookie咱們就不予設置,這樣就保障了Cookie信息的安全也保證了應用。

若是你正在使用的是兼容 Java EE 6.0 的容器,如 Tomcat 7,那麼 Cookie 類已經有了 setHttpOnly 的方法來使用 HttpOnly 的 Cookie 屬性了。

1
cookie.setHttpOnly( true );

設置完後生成的 Cookie 就會在最後多了一個 ;HttpOnly

另外使用 Session 的話 jsessionid 這個 Cookie 可經過在 Context 中使用 useHttpOnly 配置來啓用 HttpOnly,例如:

1
2
< Context path = "" docBase = "D:/WORKDIR/oschina/webapp"
     reloadable = "false" useHttpOnly = "true" />

也能夠在 web.xml 配置以下:

1
2
3
4
5
< session-config >
  < cookie-config >
   < http-only >true</ http-only >
  </ cookie-config >
< session-config >

對於 不支持 HttpOnly 的低版本java ee,能夠手動設置(好比在一個過濾器中):

String sessionid = request.getSession().getId();  
response.setHeader("SET-COOKIE", "JSESSIONID=" + sessionid + "; HttpOnly"); 

對於 .NET 2.0 應用能夠在 web.config 的 system.web/httpCookies 元素使用以下配置來啓用 HttpOnly?

1
< httpCookies httpOnlyCookies = "true" …>

而程序的處理方式以下:

C#:

1
2
3
HttpCookie myCookie = new HttpCookie( "myCookie" );
myCookie.HttpOnly = true ;
Response.AppendCookie(myCookie);

VB.NET:

1
2
3
Dim myCookie As HttpCookie = new HttpCookie( "myCookie" )
myCookie.HttpOnly = True
Response.AppendCookie(myCookie)

.NET 1.1 只能手工處理:

1
Response.Cookies[cookie].Path += ";HttpOnly" ;

PHP 從 5.2.0 版本開始就支持 HttpOnly

1
session.cookie_httponly = True

PS: 實際測試在 Tomcat 8.0.21 中在 web.xml 和 context中設置 HttpOnly 屬性不起做用。只有cookie.setHttpOnly(true); 和

 response.setHeader("SET-COOKIE", "JSESSIONID=" + sessionid+ "; HttpOnly"); 起做用

參考連接:https://www.owasp.org/index.php/HttpOnly#Using_Java_to_Set_HttpOnly

5. 總結下

XSS攻擊訪問方法:對輸入(和URL參數)進行過濾,對輸出進行編碼;白名單和黑名單結合;

--------------------------------------------分割線----------------------------------------------------

6. 使用 OWASP AntiSamy Project 和 OWASP ESAPI for Java 來防護 XSS(還有客戶端的esapi4js: esapi.js)

上面說到了防護XSS的一些原理和思想,對輸入進行過濾,對輸出進行編碼。那麼OWASP組織中項目 AntiSamy 和 ESAPI 就偏偏可以幫助咱們。其中 AntiSamy 提供了 XSS Filter 的實現,而 ESAPI 則提供了對輸出進行編碼的實現。(samy是一我的名,他第一次在MySpace上實現第一個XSS工具蠕蟲,因此AntiSamy項目就是反XSS攻擊的意思; ESAPI就是enterprise security api的意思;owasp: Open Web Application Securtiy Project)

使用maven能夠引入依賴包:

    <dependency>
        <groupId>org.owasp.antisamy</groupId>
        <artifactId>antisamy</artifactId>
        <version>1.5.3</version>
    </dependency>
    <dependency>
        <groupId>org.owasp.esapi</groupId>
        <artifactId>esapi</artifactId>
        <version>2.1.0</version>
    </dependency>

使用AntiSamy構造XSS Filter的方法以下:

public class XssFilter implements Filter {

    public XssFilter() {}

    public void destroy() {}

    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest)request;
        HttpServletResponse resp = (HttpServletResponse)response;
        resp.setHeader("SET-COOKIE", "JSESSIONID=" + req.getSession().getId()+ "; HttpOnly");
        
        chain.doFilter(new XssRequestWrapper(req), resp);
    }

    public void init(FilterConfig fConfig) throws ServletException {}
}
XssFilter
public class XssRequestWrapper extends HttpServletRequestWrapper {

    private static Policy policy = null;

    static {
        String path = XssRequestWrapper.class.getClassLoader().getResource("antisamy-anythinggoes.xml").getFile();
        System.out.println("policy_filepath:" + path);
        if (path.startsWith("file")) {
            path = path.substring(6);
        }
        try {
            policy = Policy.getInstance(path);
        } catch (PolicyException e) {
            e.printStackTrace();
        }
    }

    public XssRequestWrapper(HttpServletRequest request) {
        super(request);
    }

    public String getParameter(String paramString) {
        String str = super.getParameter(paramString);
        if (str == null)
            return null;
        return xssClean(str);
    }

    public String getHeader(String paramString) {
        String str = super.getHeader(paramString);
        if (str == null)
            return null;
        return xssClean(str);
    }
    
    @SuppressWarnings("rawtypes")
    public Map<String, String[]> getParameterMap() {
        Map<String, String[]> request_map = super.getParameterMap();
        Iterator iterator = request_map.entrySet().iterator();
        System.out.println("request_map" + request_map.size());
        while (iterator.hasNext()) {
            Map.Entry me = (Map.Entry) iterator.next();
            String[] values = (String[]) me.getValue();
            for (int i = 0; i < values.length; i++) {
                values[i] = xssClean(values[i]);
            }
        }
        return request_map;
    }

    public String[] getParameterValues(String paramString) {
        String[] arrayOfString1 = super.getParameterValues(paramString);
        if (arrayOfString1 == null)
            return null;
        int i = arrayOfString1.length;
        String[] arrayOfString2 = new String[i];
        for (int j = 0; j < i; j++)
            arrayOfString2[j] = xssClean(arrayOfString1[j]);
        return arrayOfString2;
    }

    private String xssClean(String value) {
        AntiSamy antiSamy = new AntiSamy();
        try {
            final CleanResults cr = antiSamy.scan(value, policy);
            // 安全的HTML輸出
            return cr.getCleanHTML();
        } catch (ScanException e) {
            e.printStackTrace();
        } catch (PolicyException e) {
            e.printStackTrace();
        }
        return value;
    }
}
XssRequestWrapper

而後在web.xml中配置:

  <filter>
      <filter-name>XssFilter</filter-name>
      <filter-class>org.xss.filter.XssFilter</filter-class>
  </filter>
  <filter-mapping>
      <filter-name>XssFilter</filter-name>
      <url-pattern>/*</url-pattern>
  </filter-mapping>
web.xml

上面咱們不只對輸入進行掃描過濾,並且設置了response中sessionId的httponly屬性,咱們看下httponly的實際效果:

咱們看到baidu使用了httponly=true,咱們的localhost的httponly=true也其做用了。

ESAPI 編碼輸出,使用方法入下:

        ESAPI.encoder().encodeForHTML(String input);
        ESAPI.encoder().encodeForHTMLAttribute(String input);
        ESAPI.encoder().encodeForJavaScript(String input);
        ESAPI.encoder().encodeForCSS(String input);
        ESAPI.encoder().encodeForURL(String input);
        
        MySQLCodec codec = new MySQLCodec(Mode.STANDARD);
        ESAPI.encoder().encodeForSQL(codec, String input);

對應上面 3.2 中說到的編碼輸出。encodeForSQL 用於 防護 MySQL 的 sql 注入。

(壓縮版的esapi-compressed.js,大小爲51K。)

-----------------------------------------------------

低危漏洞- X-Frame-Options Header未配置

X-Frame-Options 響應頭 
X-Frame-Options HTTP 響應頭是用來給瀏覽器指示容許一個頁面能否在 <frame>, </iframe> 或者 <object> 中展示的標記。網站可使用此功能,來確保本身網站的內容沒有被嵌到別人的網站中去,也從而避免了點擊劫持 (clickjacking) 的攻擊。

使用 X-Frame-Options 
X-Frame-Options 有三個值:

DENY 
表示該頁面不容許在 frame 中展現,即使是在相同域名的頁面中嵌套也不容許。 
SAMEORIGIN 
表示該頁面能夠在相同域名頁面的 frame 中展現。 
ALLOW-FROM uri 
表示該頁面能夠在指定來源的 frame 中展現。 
換一句話說,若是設置爲 DENY,不光在別人的網站 frame 嵌入時會沒法加載,在同域名頁面中一樣會沒法加載。另外一方面,若是設置爲 SAMEORIGIN,那麼頁面就能夠在同域名頁面的 frame 中嵌套。

配置 Apache

配置 Apache 在全部頁面上發送 X-Frame-Options 響應頭,須要把下面這行添加到 ‘site’ 的配置中:

Header always append X-Frame-Options SAMEORIGIN

配置 nginx

配置 nginx 發送 X-Frame-Options 響應頭,把下面這行添加到 ‘http’, ‘server’ 或者 ‘location’ 的配置中:

add_header X-Frame-Options SAMEORIGIN;

配置 IIS

配置 IIS 發送 X-Frame-Options 響應頭,添加下面的配置到 Web.config 文件中:

<system.webServer>
  ... <httpProtocol> <customHeaders> <add name="X-Frame-Options" value="SAMEORIGIN" /> </customHeaders> </httpProtocol> ... </system.webServer>

結果

在 Firefox 嘗試加載 frame 的內容時,若是 X-Frame-Options 響應頭設置爲禁止訪問了,那麼 Firefox 會用 about:blank 展示到 frame 中。也許從某種方面來說的話,展現爲錯誤消息會更好一點。

Tomcat處理:

定義一個過濾器filter,在HTTP請求頭中加入x-frame-options: SAMEORIGIN便可。

最後補充代碼:

/**
 * 安全過濾器
 * @author digdeep@126.com
 */
public class SecureFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        
    	HttpServletRequest req = (HttpServletRequest)request;
    	HttpServletResponse resp = (HttpServletResponse)response;
		
    	String sessionid = req.getSession().getId();  
    	resp.setHeader("SET-COOKIE", "JSESSIONID=" + sessionid + "; HttpOnly"); 
    	resp.setHeader("x-frame-options","SAMEORIGIN");	//X-Frame-Options
    	
    	chain.doFilter(request, response);
    }

	@Override
	public void init(FilterConfig arg0) throws ServletException {
		
	}
	
	@Override
    public void destroy() {

    }
}

進行配置:

  <filter>
    <filter-name>SecureFilter</filter-name>
    <filter-class>com.diantu.hemr.filter.SecureFilter</filter-class>
  </filter>
  <filter-mapping>
    <filter-name>SecureFilter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>

進行驗證生效:

相關文章
相關標籤/搜索