跨站請求僞造和cookie僞造

CSRF(Cross-site request forgery跨站請求僞造,也被稱成爲「one click attack」或者session riding,一般縮寫爲CSRF或者XSRF,是一種對網站的惡意利用。 javascript

1、CSRF攻擊原理html

CSRF攻擊原理比較簡單,如圖1所示。其中Web A爲存在CSRF漏洞的網站,Web B爲攻擊者構建的惡意網站,User C爲Web A網站的合法用戶。前端

 

跨站請求僞造CSRF防禦方法

圖1 CSRF攻擊原理java

1. 用戶C打開瀏覽器,訪問受信任網站A,輸入用戶名和密碼請求登陸網站A;python

2.在用戶信息經過驗證後,網站A產生Cookie信息並返回給瀏覽器,此時用戶登陸網站A成功,能夠正常發送請求到網站A;web

3. 用戶未退出網站A以前,在同一瀏覽器中,打開一個TAB頁訪問網站B;ajax

4. 網站B接收到用戶請求後,返回一些攻擊性代碼,併發出一個請求要求訪問第三方站點A;算法

5. 瀏覽器在接收到這些攻擊性代碼後,根據網站B的請求,在用戶不知情的狀況下攜帶Cookie信息,向網站A發出請求。網站A並不知道該請求實際上是由B發起的,因此會根據用戶C的Cookie信息以C的權限處理該請求,致使來自網站B的惡意代碼被執行。sql

2、CSRF漏洞防護數據庫

CSRF漏洞防護主要能夠從三個層面進行,即服務端的防護、用戶端的防護和安全設備的防護。

一、 服務端的防護

.1.1 驗證HTTP Referer字段

根據HTTP協議,在HTTP頭中有一個字段叫Referer,它記錄了該HTTP請求的來源地址。在一般狀況下,訪問一個安全受限頁面的請求必須來自於同一個網站。好比某銀行的轉帳是經過用戶訪問http://bank.test/test?page=10&userID=101&money=10000頁面完成,用戶必須先登陸bank.test,而後經過點擊頁面上的按鈕來觸發轉帳事件。當用戶提交請求時,該轉帳請求的Referer值就會是轉帳按鈕所在頁面的URL(本例中,一般是以bank. test域名開頭的地址)。而若是攻擊者要對銀行網站實施CSRF攻擊,他只能在本身的網站構造請求,當用戶經過攻擊者的網站發送請求到銀行時,該請求的Referer是指向攻擊者的網站。所以,要防護CSRF攻擊,銀行網站只須要對於每個轉帳請求驗證其Referer值,若是是以bank. test開頭的域名,則說明該請求是來自銀行網站本身的請求,是合法的。若是Referer是其餘網站的話,就有多是CSRF攻擊,則拒絕該請求。

1.2 在請求地址中添加token並驗證

CSRF攻擊之因此可以成功,是由於攻擊者能夠僞造用戶的請求,該請求中全部的用戶驗證信息都存在於Cookie中,所以攻擊者能夠在不知道這些驗證信息的狀況下直接利用用戶本身的Cookie來經過安全驗證。由此可知,抵禦CSRF攻擊的關鍵在於:在請求中放入攻擊者所不能僞造的信息,而且該信息不存在於Cookie之中。鑑於此,系統開發者能夠在HTTP請求中以參數的形式加入一個隨機產生的token,並在服務器端創建一個攔截器來驗證這個token,若是請求中沒有token或者token內容不正確,則認爲多是CSRF攻擊而拒絕該請求。

1.3 在HTTP頭中自定義屬性並驗證

自定義屬性的方法也是使用token並進行驗證,和前一種方法不一樣的是,這裏並非把token以參數的形式置於HTTP請求之中,而是把它放到HTTP頭中自定義的屬性裏。經過XMLHttpRequest這個類,能夠一次性給全部該類請求加上csrftoken這個HTTP頭屬性,並把token值放入其中。這樣解決了前一種方法在請求中加入token的不便,同時,經過這個類請求的地址不會被記錄到瀏覽器的地址欄,也不用擔憂token會經過Referer泄露到其餘網站。

二、 其餘防護方法

1. CSRF攻擊是有條件的,當用戶訪問惡意連接時,認證的cookie仍然有效,因此當用戶關閉頁面時要及時清除認證cookie,對支持TAB模式(新標籤打開網頁)的瀏覽器尤其重要。

2. 儘可能少用或不要用request()類變量,獲取參數指定request.form()仍是request. querystring (),這樣有利於阻止CSRF漏洞攻擊,此方法只不能徹底防護CSRF攻擊,只是必定程度上增長了攻擊的難度。

代碼示例:

Java 代碼示例

下文將以 Java 爲例,對上述三種方法分別用代碼進行示例。不管使用何種方法,在服務器端的攔截器必不可少,它將負責檢查到來的請求是否符合要求,而後視結果而決定是否繼續請求或者丟棄。在 Java 中,攔截器是由 Filter 來實現的。咱們能夠編寫一個 Filter,並在 web.xml 中對其進行配置,使其對於訪問全部須要 CSRF 保護的資源的請求進行攔截。

在 filter 中對請求的 Referer 驗證代碼以下

清單 1. 在 Filter 中驗證 Referer

 // 從 HTTP 頭中取得 Referer 值 String referer=request.getHeader("Referer"); // 判斷 Referer 是否以 bank.example 開頭 if((referer!=null) &&(referer.trim().startsWith(「bank.example」))){ chain.doFilter(request, response); }else{ request.getRequestDispatcher(「error.jsp」).forward(request,response); }

以上代碼先取得 Referer 值,而後進行判斷,當其非空並以 bank.example 開頭時,則繼續請求,不然的話多是 CSRF 攻擊,轉到 error.jsp 頁面。

若是要進一步驗證請求中的 token 值,代碼以下

清單 2. 在 filter 中驗證請求中的 token

HttpServletRequest req = (HttpServletRequest)request; HttpSession s = req.getSession(); // 從 session 中獲得 csrftoken 屬性 String sToken = (String)s.getAttribute(「csrftoken」); if(sToken == null){ // 產生新的 token 放入 session 中 sToken = generateToken(); s.setAttribute(「csrftoken」,sToken); chain.doFilter(request, response); } else{ // 從 HTTP 頭中取得 csrftoken String xhrToken = req.getHeader(「csrftoken」); // 從請求參數中取得 csrftoken String pToken = req.getParameter(「csrftoken」); if(sToken != null && xhrToken != null && sToken.equals(xhrToken)){ chain.doFilter(request, response); }else if(sToken != null && pToken != null && sToken.equals(pToken)){ chain.doFilter(request, response); }else{ request.getRequestDispatcher(「error.jsp」).forward(request,response); } }

首先判斷 session 中有沒有 csrftoken,若是沒有,則認爲是第一次訪問,session 是新創建的,這時生成一個新的 token,放於 session 之中,並繼續執行請求。若是 session 中已經有 csrftoken,則說明用戶已經與服務器之間創建了一個活躍的 session,這時要看這個請求中有沒有同時附帶這個 token,因爲請求可能來自於常規的訪問或是 XMLHttpRequest 異步訪問,咱們分別嘗試從請求中獲取 csrftoken 參數以及從 HTTP 頭中獲取 csrftoken 自定義屬性並與 session 中的值進行比較,只要有一個地方帶有有效 token,就斷定請求合法,能夠繼續執行,不然就轉到錯誤頁面。生成 token 有不少種方法,任何的隨機算法均可以使用,Java 的 UUID 類也是一個不錯的選擇。

除了在服務器端利用 filter 來驗證 token 的值之外,咱們還須要在客戶端給每一個請求附加上這個 token,這是利用 js 來給 html 中的連接和表單請求地址附加 csrftoken 代碼,其中已定義 token 爲全局變量,其值能夠從 session 中獲得。

清單 3. 在客戶端對於請求附加 token

 

Tornado 使用經驗【防範 跨站僞造請求CSRF 或 XSRF;二、防止僞造 cookie 】

最近在作一個網站的後端開發。由於初期只有我一我的作,因此技術選擇上很自由。在 web 服務器上我選擇了 。雖然曾經也讀過它的源碼,並作過一些小的 demo,但畢竟這是第一次在工做中使用,不免又發現了一些值得分享的東西。 

首先想說的是它的安全性,這方面確實能讓我感覺到它的良苦用心。這主要能夠分爲兩點: 

  1. 防範 跨站僞造請求 (Cross-site request forgery,簡稱 CSRF 或 XSRF)。 
    CSRF 的意思簡單來講就是,攻擊者僞造真實用戶來發送請求。 

    舉例來講,假設某個銀行網站有這樣的 URL: 
    http://bank.example.com/withdraw?amount=1000000&for=Eve
    當這個銀行網站的用戶訪問該 URL 時,就會給 Eve 這名用戶一百萬元。用戶固然不會輕易地點擊這個 URL,可是攻擊者能夠在其餘網站上嵌入一張僞造的圖片,將圖片地址設爲該 URL: 
    <img src="http://bank.example.com/withdraw?amount=1000000&for=Eve">
    那麼當用戶訪問那個惡意網站時,瀏覽器就會對該 URL 發起一個 GET 請求,因而在用戶絕不知情的狀況下,一百萬就被轉走了。 

    要防範上述攻擊很簡單,不容許經過 GET 請求來執行更改操做(例如轉帳)便可。不過其餘類型的請求照樣也不安全,假如攻擊者構造這樣一個表單: 
    <form action="http://bank.example.com/withdraw" method="post">  <p>轉發抽獎送 iPad 啊!</p>  <input type="hidden" name="amount" value="1000000">  <input type="hidden" name="for" value="Eve">  <input type="submit" value="轉發"> </form>
    不明真相的用戶點了下「轉發」按鈕,結果錢就被轉走了… 

    要杜絕這種狀況,就須要在非 GET 請求時添加一個攻擊者沒法僞造的字段,處理請求時驗證這個字段是否修改過。 
    Tornado 的處理方法很簡單,在請求中增長了一個隨機生成的 _xsrf 字段,而且 cookie 中也增長這個字段,在接收請求時,比較這 2 個字段的值。 
    因爲非本站的網頁是不能獲取或修改 cookie 的,這就保證了 _xsrf 沒法被第三方網站僞造(HTTP 嗅探例外)。 
    固然,用戶本身是能夠隨意獲取和修改 cookie 的,不過這已經不屬於 CSRF 的範疇了:用戶本身僞造本身所作的事情,固然由他本身來承擔。 

    要使用該功能的話,須要在生成 tornado.web.Application 對象時,加上 xsrf_cookies=True 參數,這會給用戶生成一個名爲 _xsrf 的 cookie 字段。 
    此外還須要你在非 GET 請求的表單里加上 xsrf_form_html(),若是不用 Tornado 的模板的話,在 tornado.web.RequestHandler 內部能夠用 self.xsrf_form_html() 來生成。 

    對於 AJAX 請求來講,基本上是不須要擔憂跨站的,因此 Tornado 1.1.1 之前的版本並不對帶有 X-Requested-With: XMLHTTPRequest 的請求作驗證。 
    後來 Google 的工程師指出,惡意的瀏覽器插件能夠僞造跨域 AJAX 請求,因此也應該進行驗證。對此我不置能否,由於瀏覽器插件的權限能夠很是大,僞造 cookie 或是直接提交表單都行。 
    不過解決辦法仍然要說,其實只要從 cookie 中獲取 _xsrf 字段,而後在 AJAX 請求時加上這個參數,或者放在 X-Xsrftoken 或 X-Csrftoken 請求頭裏便可。嫌麻煩的話,能夠用 jQuery 的 $.ajaxSetup() 來處理: 
    $.ajaxSetup({
     beforeSend: function(jqXHR, settings) {   type = settings.type   if (type != 'GET' && type != 'HEAD' && type != 'OPTIONS') {    var pattern = /(.+; *)?_xsrf *= *([^;" ]+)/;    var xsrf = pattern.exec(document.cookie);    if (xsrf) {     jqXHR.setRequestHeader('X-Xsrftoken', xsrf[2]);    }   } }});

    此外再順便談談 跨站腳本 (Cross-site scripting,簡稱 XSS)。和 CSRF 相反的是,XSS 是利用被攻擊網站自身的漏洞,在該網站上注入攻擊者想執行的腳本代碼,讓瀏覽該網站的用戶執行。 
    不過只要不讓用戶隨意輸入 HTML(例如對 < 和 > 進行轉義),對 HTML 元素的屬性作驗證(例如屬性裏的引號要轉義,src 和 事件處理等屬性不能隨意填寫 JavaScript 代碼等),並檢查 CSS(含 style 屬性)中的 expression 便可避免。 
  2. 防止僞造 cookie。 
    前面提到的 CSRF 和 XSS 都是攻擊者在用戶不知情的狀況下,冒用他的名義來進行操做;而僞造 cookie 則是攻擊者本身主動僞造其餘用戶來進行操做。 
    舉例來講,假設網站的登陸驗證就是檢查 cookie 中的用戶名,只要符合的話,就認爲該用戶已登陸。那麼攻擊者只要在 cookie 中設置 username=admin 之類的值,就能夠冒充管理員來操做了。 

    要防止 cookie 被僞造,首先須要提到設置 cookie 時的兩個參數: secure 和 httponly 。這兩個參數並不在 tornado.web.RequestHandler.set_cookie() 的參數列表裏,而是做爲關鍵字參數傳遞,並在 Cookie.Morsel._reserved 中定義的。 
    前者是指這個 cookie 只能經過安全鏈接傳遞(即 HTTPS),這就使得嗅探者沒法截獲該 cookie;後者則要求其只能在 HTTP 協議下訪問(即沒法經過 JavaScript 來獲取 document.cookie 中的該字段,而且設置後也不會經過 HTTP 協議向服務器發送),這便使得攻擊者沒法簡單地經過 JavaScript 腳原本僞造 cookie。 

    不過對於惡意的攻擊者,這兩個參數並不能杜絕 cookie 被僞造。爲此就須要對 cookie 作個簽名,一旦被修改,服務器端能夠判斷出來。 
    Tornado 中提供了 set_secure_cookie() 這個方法來對 cookie 作簽名。簽名時須要提供一串祕鑰(生成 tornado.web.Application 對象時的 cookie_secret 參數),這個祕鑰能夠經過以下代碼來生成: 
    base64.b64encode(uuid.uuid4().bytes + uuid.uuid4().bytes)
    這個參數能夠隨機生成,但若是同時有多個 Tornado 進程來服務的話,或者有時會重啓的話,仍是共用一個常量比較好,而且注意不要泄露。 

    這個簽名用的是 HMAC 算法 ,hash 算法採用的是 SHA1。簡單來講就是把 cookie 名、值和時間戳的 hash 做爲簽名,再把「值|時間戳|簽名」做爲新的值。這樣服務器端只要拿祕鑰再次加密,比較簽名是否有變化過便可判斷真僞。 
    值得一提的是讀源碼時還發現這樣一個函數: 
    def _time_independent_equals(a, b): if len(a) != len(b): return False result = 0 if type(a[0]) is int: # python3 byte strings for x, y in zip(a, b): result |= x ^ y else: # python2 for x, y in zip(a, b): result |= ord(x) ^ ord(y) return result == 0
    讀了半天也沒發現和普通的字符串比較有什麼優勢,直到看了 StackOverflow 上的答案才知道:爲了不攻擊者經過測試比較時間來判斷正確的位數,這個函數讓比較的時間比較恆定,也就杜絕了這種狀況。(話說這答案看得我各類佩服啊,搞安全的專家果真不是我那麼膚淺的…) 

接着是繼承 tornado.web.RequestHandler。 
在執行流程上,tornado.web.Application 會根據 URL 尋找一個匹配的 RequestHandler 類,並初始化它。它的 __init__() 方法會調用 initialize() 方法,因此只要覆蓋後者便可,而且不須要調用父類的 initialize()。 
接着根據不一樣的 HTTP 方法尋找該 handler 的 get/post() 等方法,並在執行前運行 prepare()。這些方法都不會主動調用父類的,所以有須要時,自行調用吧。 
最後會調用 handler 的 finish() 方法,這個方法最好別覆蓋。它會調用 on_finish() 方法,它能夠被覆蓋,用於處理一些善後的事情(例如關閉數據庫鏈接),但不能再向瀏覽器發送數據了(由於 HTTP 響應已發送,鏈接也可能已被關閉)。 

順便說下怎麼處理錯誤頁面。 
簡單來講,執行 RequestHandler 的 _execute() 方法(內部依次執行 prepare()、get() 和 finish() 等方法)時,任何未捕捉的錯誤都會被它的 write_error() 方法捕捉,所以覆蓋這個方法便可: 
class RequestHandler(tornado.web.RequestHandler): def write_error(self, status_code, **kwargs): if status_code == 404: self.render('404.html') elif status_code == 500: self.render('500.html') else: super(RequestHandler, self).write_error(status_code, **kwargs)
因爲歷史緣由,你也能夠覆蓋 get_error_html() 方法,不過不被推薦。 
此外,你還可能沒到 _execute() 方法就出錯了。 
例如 initialize() 方法拋出了一個未捕捉的異常,這個異常會被 IOStream 捕捉到,而後直接關閉鏈接,不能向用戶輸出任何錯誤頁面。 
再好比沒有找到一個能處理該請求的 handler,就會用 tornado.web.ErrorHandler 去處理 404 錯誤。這種狀況能夠替換這個類來實現自定義錯誤頁面: 
class PageNotFoundHandler(RequestHandler): def get(self): raise tornado.web.HTTPError(404) tornado.web.ErrorHandler = PageNotFoundHandler
另外一種方法就是在 Application 的 handlers 參數的最後,加上一個能捕捉任何 URL 的 handler: 
application = tornado.web.Application([ # ... ('.*', PageNotFoundHandler) ])

接着說說處理登陸。 
Tornado 提供了 @tornado.web.authenticated 這個裝飾器,在 handler 的 get() 等方法前加上便可。 
它會依賴三處代碼: 
  1. 須要定義 handler 的 get_current_user() 方法,例如: 
    def get_current_user(self): return self.get_secure_cookie('user_id', 0)
    它的返回值爲假時,就會跳轉到登陸頁面了。 
  2. 建立 application 時設置 login_url 參數: 
    application = tornado.web.Application( [ # ... ], login_url = '/login' )
  3. 定義 handler 的 get_login_url() 方法。 
    若是不能使用默認的 login_url 參數(例如普通用戶和管理員須要不一樣的登陸地址),那麼能夠覆蓋 get_login_url() 方法: 
    class AdminHandler(RequestHandler): def get_login_url(self): return '/admin/login'
順帶一提,跳轉到登陸頁後時會附帶一個 next 參數,指向登陸前訪問的網址。爲達到更好的用戶體驗,須要在登陸後跳轉到該網址: 
class LoginHandler(RequestHandler): def get(self): if self.get_current_user(): self.redirect('/') return self.render('login.html') def post(self): if self.get_current_user(): raise tornado.web.HTTPError(403) # check username and password if success: self.redirect(self.get_argument('next', '/'))
此外,我不少地方都使用了 AJAX 技術,而前端懶得去處理 403 錯誤,因此我只能改造一下 authenticated() 了: 
def authenticated(method): """Decorate methods with this to require that the user be logged in.""" @functools.wraps(method) def wrapper(self, *args, **kwargs): if not self.current_user: if self.request.headers.get('X-Requested-With') == 'XMLHttpRequest': # jQuery 等庫會附帶這個頭 self.set_header('Content-Type', 'application/json; charset=UTF-8') self.write(json.dumps({'success': False, 'msg': u'您的會話已過時,請從新登陸!'})) return if self.request.method in ("GET", "HEAD"): url = self.get_login_url() if "?" not in url: if urlparse.urlsplit(url).scheme: # if login url is absolute, make next absolute too next_url = self.request.full_url() else: next_url = self.request.uri url += "?" + urllib.urlencode(dict(next=next_url)) self.redirect(url) return raise tornado.web.HTTPError(403) return method(self, *args, **kwargs) return wrapper

而後說下獲取用戶的 IP 地址。 
簡單來講,在 handler 的方法裏用 self.request.remote_ip 就能拿到了。 
不過若是使用了反向代理,拿到的就是代理的 IP 了,這時候就須要在建立 HTTPServer 時增長 xheaders 的設置了: 
if __name__ == '__main__': from tornado.httpserver import HTTPServer from tornado.netutil import bind_sockets sockets = bind_sockets(80) server = HTTPServer(application, xheaders=True) server.add_sockets(sockets) tornado.ioloop.IOLoop.instance().start()
此外,我只須要處理 IPv4,但本地測試時會拿到 ::1 這種 IPv6 地址,因此還須要設置一下: 
if settings.IPV4_ONLY: import socket sockets = bind_sockets(80, family=socket.AF_INET) else: sockets = bind_sockets(80)

最後再提下生產環境下如何提升性能。Tornado 能夠在 HTTPServer 調用 add_sockets() 前建立多個子進程,利用多 CPU 的優點來處理併發請求。 
簡單來講,代碼以下: 
if __name__ == '__main__': if settings.IPV4_ONLY: import socket sockets = bind_sockets(80, family=socket.AF_INET) else: sockets = bind_sockets(80) if not settings.DEBUG_MODE: import tornado.process tornado.process.fork_processes(0) # 0 表示按 CPU 數目建立相應數目的子進程 server = HTTPServer(application, xheaders=True) server.add_sockets(sockets) tornado.ioloop.IOLoop.instance().start()
注意這種方式下不能啓用 autoreload 功能(application 在建立時,debug 參數不能爲真)。
相關文章
相關標籤/搜索