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網站的合法用戶。前端
圖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
最近在作一個網站的後端開發。由於初期只有我一我的作,因此技術選擇上很自由。在 web 服務器上我選擇了 。雖然曾經也讀過它的源碼,並作過一些小的 demo,但畢竟這是第一次在工做中使用,不免又發現了一些值得分享的東西。
首先想說的是它的安全性,這方面確實能讓我感覺到它的良苦用心。這主要能夠分爲兩點:
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 請求,因而在用戶絕不知情的狀況下,一百萬就被轉走了。 <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>
不明真相的用戶點了下「轉發」按鈕,結果錢就被轉走了… $.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]); } } }});
base64.b64encode(uuid.uuid4().bytes + uuid.uuid4().bytes)這個參數能夠隨機生成,但若是同時有多個 Tornado 進程來服務的話,或者有時會重啓的話,仍是共用一個常量比較好,而且注意不要泄露。
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 上的答案才知道:爲了不攻擊者經過測試比較時間來判斷正確的位數,這個函數讓比較的時間比較恆定,也就杜絕了這種狀況。(話說這答案看得我各類佩服啊,搞安全的專家果真不是我那麼膚淺的…) 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() 方法,不過不被推薦。
class PageNotFoundHandler(RequestHandler): def get(self): raise tornado.web.HTTPError(404) tornado.web.ErrorHandler = PageNotFoundHandler
另外一種方法就是在 Application 的 handlers 參數的最後,加上一個能捕捉任何 URL 的 handler:
application = tornado.web.Application([ # ... ('.*', PageNotFoundHandler) ])
def get_current_user(self): return self.get_secure_cookie('user_id', 0)
它的返回值爲假時,就會跳轉到登陸頁面了。 application = tornado.web.Application( [ # ... ], login_url = '/login' )
class AdminHandler(RequestHandler): def get_login_url(self): return '/admin/login'
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
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)
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 參數不能爲真)。