在2013與2017即將正式公布的OWASP Top 10中,跨站偽造請求(Cross-site Request Forgery,CSRF)都佔有一席之地,然而相對於開發者多半都略知一二的SQL Injection、XSS來說,CSRF是比較容易被忽略或誤解的一種安全問題,多數的例子看似URL設計失誤,實際上,是單純信任瀏覽器,忽略使用者身分確認機制的結果。

認識CSRF漏洞

XSS往往容易與CSRF產生混淆,原因在於XSS與CSRF都是跨站式的請求攻擊,有些CSRF攻擊是利用XSS來實現,然而,CSRF不一定要透過XSS,使用者在無知的情況下,點選某外部網站的鏈結(甚至只是瀏覽了某個頁面)回到已登入網站,即使在沒有執行任何JavaScript的情況下,也能使CSRF攻擊成立,因此CSRF也有著One-click Attack的別稱。

CSRF要能成立的必要條件是,使用者已登入網站,而最簡單的場景就是,使用者登入後,單憑瀏覽器與伺服端之間的會話溝通,就確認使用者的身分無誤而進行各種操作,因而使得有意攻擊者,只要能命令瀏覽器做出想要的請求,就能實現攻擊。

最常見也最容易的攻擊範例,會以不正確的URL設計作為開端,像是若要刪除某個文章,單純使用了/delete?articleId=1234這樣的GET請求,而且在實際刪除文章之前,沒有任何一步再確認的動作,這麼一來,攻擊者只要在其他網站,設法引誘已登入使用者點選<a href='user_site/delete?articleId=1234'>美女走光圖</a>這類的鏈結,使用者就會在不知情的情況下,刪除了某篇文章。

進一步地,攻擊者也可以利用圖片的src屬性,實現單是瀏覽頁面就讓伺服端上的文章消失之結果。然而,不是改成POST請求就沒事了,透過JavaScript也能發送POST請求,這可能是攻擊者在某網站上寫了JavaScript,或者利用XSS注入JavaScript來實現,歷史上,在MySpace上產生的首個XSS蠕蟲Samy,實際上就是XSS與CSRF結合的結果,一開始只是在Samy的自傳頁面上,寫了段JavaScript,若有人瀏覽了該頁面,就會在使用者自身的頁面上,自動加入samy is my hero與散播用的JavaScript。

防禦的基本原理

使用者必須是在登入狀態,CSRF才有可能成立,因此,儘量不使用自動登入,在使用者沒有活動一段時間之後,自動登出、令會話失效,可以減少CSRF攻擊成功的可能性。

不正確的URL設計,確實是降低了CSRF的攻擊難度,HTTP的GET請求被用在能變動伺服器的操作上(甚至更糟的,同一個URL只憑請求參數來決定是否變動伺服端狀態)是個不良的設計,正如我先前專欄〈重新認識HTTP請求方法〉中談過的,開發者得區分安全與等冪方法(而不是單純區分GET與POST),注意URL設計時的一致性,如此才能在會變動伺服端狀態的操作上,從事進一步的防護。

在URL上,能清楚區分安全與等冪操作之後,接下來,就可以針對非安全的操作,確認到底是否基於使用者的意願下發出請求,或者是攻擊者利用了某種手法令瀏覽器發出。簡單來說,網站開發者越是相信瀏覽器,就容易受到攻擊——當瀏覽器拿著某個Session Id,聲稱它代表某個使用者會話,開發者就單純地信任其請求,就是最糟糕的情況。

增加操作前的確認畫面,是增加CSRF攻擊難度的一個方式,不過若單純只有「確認」按鈕,本質上仍只檢查Session Id的話,也是沒用,較有效的確認畫面必須有其他驗證方式,像是再次輸入密碼(最好是有別於登入時使用之密碼或透過第三方傳遞的一次性密碼)、發送電子郵件附上驗證鏈結等。

檢查Referer,確認來源非跨站請求,是個相對簡單的防禦方式,然而,檢查規則容易遺漏或規避,而有些軟體或瀏覽器可以控制傳送或停用Referer,因而這種方式只能用在暫時性防禦,或者是已知使用者環境的情況下。

增加一個額外的隨機產生令牌(Token),確認只有使用者自身意願下發出請求,才會附上令牌,若攻擊者利用了某種手法,令瀏覽器發出的情況,則不會附上令牌。這是個公認有效的防禦方式,通常稱之為Synchronizer Token模式,許多程式庫或框架都提供了相關實作,使用方式也略有不同。

Synchronizer Token

以Java為例,Tomcat提供CsrfPreventionFlilter,可以在能區分安全與非安全URL的場合下使用,開發者必須在web.xml中定義CsrfPreventionFlilter過濾器,設定需要產生令牌的頁面(透過entryPoints初始參數),以及需要驗證令牌的頁面。對於請求的URL,必須使用response的encodeURL,對URL加以編碼,而且,令牌將以GET請求,附上?org.apache.catalina.filters.CSRF_NONCE=82611635C05A041098781079594C02F6的方式傳遞。

OWASP也提供CSRFGuard專案,同樣須先設定過濾器,然而提供JavaScript注入與JSP標籤庫等方式。

JavaScript的注入,主要是透過伺服端動態產生的JavaScript,在客戶端頁面執行時會自動註冊window.onload事件、搜尋頁面中的表單(使用隱藏欄位)、href、src屬性等,並產生對應的令牌與形式。如果想自行控制令牌的注入與形式,可以透過JSP標籤庫,使用<csrf:tokenname/>、<csrf:tokenvalue>、<csrf:token><csrf:a>等標籤。

在Tomcat與CSRFGuard做法下,伺服端必須產生並儲存令牌,並比對後續客戶端的請求中夾帶的令牌,是否相符,如果能確認攻擊者無法令瀏覽器發出「比對用的令牌」,那麼,比對用的令牌就不一定要儲存在伺服端,而可以是在客戶端。

Double Submit Cookie就是基於這樣的概念,比對用的令牌是儲存在Cookie中,伺服器會收到請求參數中有令牌,並與Cookie中送出的令牌相比對,如果沒有比對用的令牌,或是令牌不符就拒絕操作。例如,Django就是基於此原理,來實現CSRF防禦。

當然,Double Submit Cookie是在Cookie的domain正確設定,而且攻擊者無法控制子網域的情況下,才能有效防禦。但在這個情況成立下,一個衍生自Double Submit Cookie的有趣方案,是Stateless CSRF Protection(https://goo.gl/dWGKJ7),特性在於,連比對用的令牌,都是客戶端產生並儲存於Cookie,如此一來,伺服端連令牌的產生都不用,只需比對Cookie與請求參數中的令牌即可。

加強使用者身分辨認機制

CSRF必須是在使用者已登入的情況下,才有可能使攻擊成立,然而,攻擊者實際上不用取得Session Id,這是因為不管是否來自同一個網站之請求,瀏覽器都會自動與伺服端之間進行會話。

既然如此,有沒有可能直接從瀏覽器上加強安全機制呢?目前Chrome與Opera上實現了SameSite(https://goo.gl/WP45Vo),透過在Cookie中加入SameSite,跨站請求時,就不會送上Cookie(Session Id預設使用Cookie傳遞)。

SameSite機制簡單來說,就是在跨站請求時,讓伺服端無法從Cookie中取得Session Id,從而使得單純憑藉Session Id驗證身分,就可進行各種操作脆弱網站得到保護,姑且不論瀏覽器的支援度,這實際上是個消極的防禦機制,從這一點來看,防範CSRF最根本的機制,就是不要單純信任Session Id,而必須有其他可再確認請求者身分的管道。

先前談論的各種方式,也都是如此,加強使用者身分辨認機制才是防範CSRF的正確方向,然而我們必須明白:任何自動化的驗證機制,也有可能產生自動化的偽造。使用了Synchronizer Token,也不是就萬無一失了,搜尋一下「csrf token bypass」,進一步思索使用者身分辨認該做到哪種程度,必要的時候,不採用自動化驗證來防禦,雖然可能造成使用者些許不便,卻也是可考慮的方向。

專欄作者

熱門新聞

Advertisement