起初我一直弄不清楚 CSRF 究竟和 XSS 有什麼區別,後來才明白 CSRF 和 XSS 根本是兩個不一樣維度上的分類。XSS 是實現 CSRF 的諸多途徑中的一條,但絕對不是惟一的一條。通常習慣上把經過 XSS 來實現的 CSRF 稱爲 XSRF。php
CSRF 的全稱是「跨站請求僞造」,而 XSS 的全稱是「跨站腳本」。看起來有點類似,它們都是屬於跨站攻擊——不攻擊服務器端而攻擊正常訪問網站的用戶,但前面說了,它們的攻擊類型是不一樣維度上的分 類。CSRF 顧名思義,是僞造請求,冒充用戶在站內的正常操做。咱們知道,絕大多數網站是經過 cookie 等方式辨識用戶身份(包括使用服務器端 Session 的網站,由於 Session ID 也是大多保存在 cookie 裏面的),再予以受權的。因此要僞造用戶的正常操做,最好的方法是經過 XSS 或連接欺騙等途徑,讓用戶在本機(即擁有身份 cookie 的瀏覽器端)發起用戶所不知道的請求。jquery
嚴格意義上來講,CSRF 不能分類爲注入攻擊,由於 CSRF 的實現途徑遠遠不止 XSS 注入這一條。經過 XSS 來實現 CSRF 易如反掌,但對於設計不佳的網站,一條正常的連接都能形成 CSRF。ajax
例如,一論壇網站的發貼是經過 GET 請求訪問,點擊發貼以後 JS 把發貼內容拼接成目標 URL 並訪問:跨域
http://example.com/bbs/create_post.php?title=標題&content=內容
瀏覽器
那麼,我只須要在論壇中發一帖,包含一連接:安全
http://example.com/bbs/create_post.php?title=我是腦殘&content=哈哈
服務器
只要有用戶點擊了這個連接,那麼他們的賬戶就會在不知情的狀況下發布了這一帖子。可能這只是個惡做劇,可是既然發貼的請求能夠僞造,那麼刪帖、轉賬、改密碼、發郵件全均可以僞造。cookie
如何解決這個問題,咱們是否能夠效仿上文應對 XSS 的作法呢?過濾用戶輸入, 不容許發佈這種含有站內操做 URL 的連接。這麼作可能會有點用,但阻擋不了 CSRF,由於攻擊者能夠經過 QQ 或其餘網站把這個連接發佈上去,爲了假裝可能還使用 bit.ly 壓縮一下網址,這樣點擊到這個連接的用戶仍是同樣會中招。因此對待 CSRF ,咱們的視角須要和對待 XSS 有所區別。CSRF 並不必定要有站內的輸入,由於它並不屬於注入攻擊,而是請求僞造。被僞造的請求能夠是任何來源,而非必定是站內。因此咱們惟有一條路可行,就是過濾請求的 處理者。併發
比較頭痛的是,由於請求能夠從任何一方發起,而發起請求的方式多種多樣,能夠經過 iframe、ajax(這個不能跨域,得先 XSS)、Flash 內部發起請求(老是個大隱患)。因爲幾乎沒有完全杜絕 CSRF 的方式,咱們通常的作法,是以各類方式提升攻擊的門檻。工具
首先能夠提升的一個門檻,就是改良站內 API 的設計。對於發佈帖子這一類建立資源的操做,應該只接受 POST 請求,而 GET 請求應該只瀏覽而不改變服務器端資源。固然,最理想的作法是使用 REST 風格 的 API 設計,GET、POST、PUT、DELETE 四種請求方法對應資源的讀取、建立、修改、刪除。如今的瀏覽器基本不支持在表單中使用 PUT 和 DELETE 請求方法,咱們可使用 ajax 提交請求(例如經過 jquery-form 插件,我最喜歡的作法),也可使用隱藏域指定請求方法,而後用 POST 模擬 PUT 和 DELETE (Ruby on Rails 的作法)。這麼一來,不一樣的資源操做區分的很是清楚,咱們把問題域縮小到了非 GET 類型的請求上——攻擊者已經不可能經過發佈連接來僞造請求了,但他們仍能夠發佈表單,或者在其餘站點上使用咱們肉眼不可見的表單,在後臺用 js 操做,僞造請求。
接下來咱們就能夠用比較簡單也比較有效的方法來防護 CSRF,這個方法就是「請求令牌」。讀過《J2EE 核心模式》的同窗應該對「同步令牌」應該不會陌生,「請求令牌」和「同步令牌」原理是同樣的,只不過目的不一樣,後者是爲了解決 POST 請求重複提交問題,前者是爲了保證收到的請求必定來自預期的頁面。實現方法很是簡單,首先服務器端要以某種策略生成隨機字符串,做爲令牌(token), 保存在 Session 裏。而後在發出請求的頁面,把該令牌以隱藏域一類的形式,與其餘信息一併發出。在接收請求的頁面,把接收到的信息中的令牌與 Session 中的令牌比較,只有一致的時候才處理請求,不然返回 HTTP 403 拒絕請求或者要求用戶從新登錄驗證身份。
請求令牌雖然使用起來簡單,但並不是不可破解,使用不當會增長安全隱患。使用請求令牌來防止 CSRF 有如下幾點要注意:
雖然請求令牌原理和驗證碼有類似之處,但不該該像驗證碼同樣,全局使用一個 Session Key。由於請求令牌的方法在理論上是可破解的,破解方式是解析來源頁面的文本,獲取令牌內容。若是全局使用一個 Session Key,那麼危險係數會上升。原則上來講,每一個頁面的請求令牌都應該放在獨立的 Session Key 中。咱們在設計服務器端的時候,能夠稍加封裝,編寫一個令牌工具包,將頁面的標識做爲 Session 中保存令牌的鍵。
在 ajax 技術應用較多的場合,由於頗有請求是 JavaScript 發起的,使用靜態的模版輸出令牌值或多或少有些不方便。但不管如何,請不要提供直接獲取令牌值的 API。這麼作無疑是鎖上了大門,卻又把鑰匙放在門口,讓咱們的請求令牌退化爲同步令牌。
第一點說了請求令牌理論上是可破解的,因此很是重要的場合,應該考慮使用驗證碼(令牌的一種升級,目前來看破解難度極大),或者要求用戶再次輸入密碼(亞馬遜、淘寶的作法)。但這兩種方式用戶體驗都很差,因此須要產品開發者權衡。
不管是普通的請求令牌仍是驗證碼,服務器端驗證過必定記得銷燬。忘記銷燬用過的令牌是個很低級可是殺傷力很大的錯誤。咱們學校的選課系統就有這個 問題,驗證碼用完並未銷燬,故只要獲取一次驗證碼圖片,其中的驗證碼能夠在屢次請求中使用(只要再也不次刷新驗證碼圖片),一直用到 Session 超時。這也是爲什麼選課系統加了驗證碼,外掛軟件升級一次以後仍然暢通無阻。