瀏覽器家族的安全反擊戰(zhàn)
1.前言
上次我說(shuō)過(guò),我們?yōu)g覽器的主要工作就是把HTML,JavaScript,CSS等文件從服務(wù)器端取下來(lái),然后解析、渲染,展示成美奐美倫的頁(yè)面呈現(xiàn)給人類。
我們還替人類保護(hù)一個(gè)叫做Cookie的小東西,網(wǎng)站會(huì)把Cookie發(fā)送給瀏覽器讓我們保存起來(lái),等到訪問(wèn)同一個(gè)網(wǎng)站的時(shí)候,我們?cè)侔阉l(fā)過(guò)去。
這個(gè)Cookie用來(lái)證明某個(gè)用戶已經(jīng)和服務(wù)器交互過(guò),更重要的是證明已經(jīng)登錄過(guò)系統(tǒng),不用再次登錄了。
JavaScript這小子在我們這里承擔(dān)了越來(lái)越重要的職責(zé),從對(duì)DOM樹(shù)的改動(dòng),到利用AJAX訪問(wèn)服務(wù)器端,他可以說(shuō)是風(fēng)光無(wú)限。
(詳情參見(jiàn)上一篇文章《瀏覽器:一個(gè)家族的奮斗》)
可是我們都忽視了一個(gè)重要的問(wèn)題:安全,這個(gè)東西差點(diǎn)讓我們家族遭受滅頂之災(zāi)。
2.Cookie失竊
有一天,我的主人登錄了"愛(ài)存不存"銀行(www.icbc.com),這個(gè)銀行網(wǎng)站給我發(fā)了一個(gè)Cookie,證明主人登錄過(guò)了, 主人在“愛(ài)存不存”銀行網(wǎng)站做了一些操作,但是忘記了退出,然后開(kāi)了一個(gè)新的Tab頁(yè)訪問(wèn)了一個(gè)叫做www.beauty.com ,我知道這個(gè)網(wǎng)站不懷好意,拼命地提醒主人,但是他仍然經(jīng)不住網(wǎng)站上那些圖片的誘惑,執(zhí)意把這個(gè)網(wǎng)站打開(kāi)。
我沒(méi)有辦法,只好下載這個(gè)不懷好意網(wǎng)站的HTML,JavaScript,CSS, 讓我沒(méi)有想到的是這里的JavaScript竟然想訪問(wèn)“愛(ài)存不存”銀行的Cookie。
“這個(gè)Cookie是愛(ài)存不存銀行給我的,不屬于www.beauty.com,你為啥要訪問(wèn)?” 我問(wèn)他。
“沒(méi)事,我好奇,想看看別的網(wǎng)站的Cookie長(zhǎng)什么樣” 他輕松地回答。
我將信將疑地把Cookie給了他,他不知道做了什么花樣,似乎是往www.beauty.com發(fā)了一個(gè)請(qǐng)求,然后就把Cookie還給了我。
很快我的主人就發(fā)現(xiàn),他在“愛(ài)存不存”銀行的私房錢(qián)不翼而飛了。
FireFox嘲笑我說(shuō):“你這個(gè)家伙啊,怎么能夠把Cookie這么重要的東西隨隨便便地給別人呢? ‘愛(ài)存不存’銀行的Cookie被黑客偷走了,那些黑客不用登錄就可以冒充用戶在‘愛(ài)存不存’網(wǎng)站做操作了。”
“啊? 有這么嚴(yán)重? 可他是JavaScript,照理說(shuō)可以訪問(wèn)啊?”
“唉,你要知道,這個(gè)JavaScript和那個(gè)Cookie不是同一個(gè)網(wǎng)站的,怎么能訪問(wèn)呢。”
由于這件事,主人再我不理我了,從此開(kāi)始寵幸FireFox。
3.密碼失竊
FireFox也沒(méi)得意很久,他也很快中了招。
這一次,主人還是忍不住去www.beauty.com看圖片,F(xiàn)ireFox這次很小心,不把任何別的網(wǎng)站的Cookie發(fā)給這里的JavaScript。
但這一次beauty.com改變了策略,它用iframe的方式放置了一個(gè)淘寶的登錄網(wǎng)頁(yè)到beauty.com頁(yè)面中,淘寶恰恰是主人最喜歡的,主人一看,不錯(cuò)啊,還有快捷登錄方式,于是主人就輸入了自己真實(shí)的用戶名和密碼,沒(méi)想到Beauty.com的JavaScript 已經(jīng)把這個(gè)淘寶登錄Form的action指向了自家網(wǎng)站,等到主人點(diǎn)了登錄按鈕以后, 用戶名和明文的密碼就這樣被竊取了。
于是FireFox也被打入冷宮。
4.家族會(huì)議
黑客猖獗,類似的安全事故不斷出現(xiàn),我們家族的成員紛紛中招,家族趕緊召集會(huì)議,商量對(duì)策,防止人類把我們家族給廢掉。
我和FireFox在會(huì)議上聲討現(xiàn)在的人類實(shí)在是喜歡訪問(wèn)那些不良網(wǎng)站,族長(zhǎng)Mozilla說(shuō)沒(méi)辦法這是人類的本性,無(wú)論如何也無(wú)法改變,如果改了就不是人類了。
“雖然我們控制不了人類的行為,但是我們?yōu)g覽器家族可以做點(diǎn)改變,增加安全性!” Mozilla族長(zhǎng)充滿正義感和使命感,他下達(dá)了一個(gè)命令:“以后我們家族確定一條鐵規(guī):除非兩個(gè)網(wǎng)頁(yè)是來(lái)自于統(tǒng)一‘源頭’, 否則不允許一個(gè)網(wǎng)頁(yè)的JavaScript訪問(wèn)另外一個(gè)網(wǎng)頁(yè)的內(nèi)容,像Cookie,DOM,LocalStorage統(tǒng)統(tǒng)禁止訪問(wèn)!”
我仔細(xì)咂摸這句話的含義,其實(shí)是說(shuō)各個(gè)網(wǎng)頁(yè)如果不同源的,就被隔離了,只能在自己的一畝三分地中折騰。
“什么叫同一個(gè)源頭?” FireFox問(wèn)道。
“就是說(shuō){protocol,host,port} 這三個(gè)東西必須得一樣! 我給你們舉個(gè)例子, 例如有這么一個(gè)網(wǎng)頁(yè): http://www.store.com/product/page.html, 下面的表格列出了各種不同情況。”
這個(gè)同源策略確實(shí)嚴(yán)格, 不同源的網(wǎng)頁(yè)無(wú)法訪問(wèn)另外一個(gè)網(wǎng)頁(yè)的DOM,Cookie, 像beauty.com那樣的惡意網(wǎng)站想偷走Cookie/密碼就不容易了。
我想到了主人之前購(gòu)物經(jīng)常訪問(wèn)的http://www.store.com/, 這個(gè)頁(yè)面中有一段裝載jquery.js的代碼:
- <script src="//static.store.com/jquery.js > </script>
這個(gè)jquery.js是來(lái)自于不同的源(static.store.com), 難道他就沒(méi)法操作www.store.com頁(yè)面的內(nèi)容了嗎? 如果不能操作,這個(gè)jquery.js就沒(méi)有任何用處啦!
我把這個(gè)困惑給大家說(shuō)了下, FireFox馬上附和: “沒(méi)錯(cuò),難道我們要強(qiáng)制人類把所有的JavaScript代碼放到www.store.com下嗎? 人類肯定不能容忍! ”
“嗯,這是個(gè)好問(wèn)題” Mozilla族長(zhǎng)說(shuō),“這樣,我給你們開(kāi)個(gè)口子,對(duì)于使用<script src='xxxxx'> 加載的JavaScript,我們認(rèn)為它的源屬于www.store.com, 而不屬于static.store.com,這樣就可以操作www.store.com的頁(yè)面了,行不行?”
我和FireFox都表示贊同,其實(shí)這種“嵌入式”的跨域加載資源的方式還有<img>,<link>等,相當(dāng)于我們?yōu)g覽器發(fā)起了一次GET請(qǐng)求,取到相關(guān)資源,然后放到本地而已。
同源策略就這么確定下來(lái)了, 我們這些瀏覽器都開(kāi)始嚴(yán)格遵守執(zhí)行,果然,安全事故大大減少。
5.凡事都有例外
過(guò)了一段時(shí)間,JavaScript這小子便跑來(lái)抱怨了:“你們這個(gè)同源策略實(shí)在太嚴(yán)格了,太不方便了。”
我說(shuō):“這也是為了大家好啊,省得那些不良網(wǎng)站干壞事。”
JavaScript說(shuō):“好什么好? 現(xiàn)在人類的系統(tǒng)越來(lái)越大,大部分都拆分成分布式的了,每個(gè)系統(tǒng)都有子域名,像login.store.com, payment.store.com, 雖然二級(jí)域名不同,但是他們屬于一個(gè)大的系統(tǒng),由于同源策略的鐵規(guī),cookie不能在這個(gè)系統(tǒng)之間共享,麻煩死了。”
這確實(shí)是個(gè)問(wèn)題,我?guī)е鳭avaScript找到到Mozilla組長(zhǎng),說(shuō)明情況,希望他網(wǎng)開(kāi)一面。
族長(zhǎng)說(shuō):“好吧,我再給你們開(kāi)個(gè)口子,如果兩個(gè)網(wǎng)頁(yè)的一級(jí)域名是相同的,他們可以共享cookie, 不過(guò)cookie的domain一定要設(shè)置為那個(gè)一級(jí)域名才可以,例如:”
- document.cookie = 'test=true;path=/;domain=store.com'
JavaScript說(shuō):“還有個(gè)大問(wèn)題,你們?yōu)槭裁床蛔屛沂褂肵MLHttpRequest訪問(wèn)不同的網(wǎng)站啊!”
我心想JavaScript說(shuō)的可能是AJAX,可以創(chuàng)建一個(gè)XMLHttpRequest對(duì)象去異步的訪問(wèn)服務(wù)器端提供的服務(wù),做到局部刷新頁(yè)面,用戶體驗(yàn)很好。
難道這個(gè)XMLHttpRequest對(duì)象只能訪問(wèn)源服務(wù)器(如book.com),不能訪問(wèn)其他服務(wù)器(如beauty.com)?
族長(zhǎng)說(shuō):“是啊,XMLHttpReqeust也要遵守同源策略。”
JavaScript說(shuō):“可是這沒(méi)有道理啊!我雖然是從book.com來(lái)的,但是為什么不讓我訪問(wèn)beauty.com?”
族長(zhǎng)說(shuō):“我這也是為了防止黑客攻擊,給你舉個(gè)例子,假設(shè)你的主人登錄了book.com , 然后又去訪問(wèn)beauty.com,如果這個(gè)beauty.com是個(gè)惡意網(wǎng)站,它也要求你創(chuàng)建一個(gè)XMLHttpRequest對(duì)象,通過(guò)這個(gè)對(duì)象向book.com(不同源)發(fā)起請(qǐng)求,獲取你主人的賬戶信息,會(huì)發(fā)生什么情況? ”
JavaScript恍然大悟:“奧,我懂了,由于主人登錄過(guò)了book.com,登錄cookie什么的都在,那beauty.com的JavaScript 向book.com發(fā)起的XMLHttpRequest請(qǐng)求也會(huì)成功,我主人的賬戶信息就會(huì)被黑客給獲取了。”
我說(shuō):“看來(lái)對(duì)XMLHttpReqeust對(duì)象施加同源策略也是非常重要的啊!”
JavaScript沉默了半天說(shuō):“那怎么辦?”
Mozilla族長(zhǎng)說(shuō):“你可以通過(guò)服務(wù)器端中轉(zhuǎn)啊,例如你是來(lái)自book.com的, 現(xiàn)在想訪問(wèn)movie.com,那可以讓那個(gè)book.com把請(qǐng)求轉(zhuǎn)發(fā)給movie.com嘛!人類好像給這種方式起了個(gè)名字,叫什么代理模式,那個(gè)book.com就是代理人。”
JavaScript急忙說(shuō):“不不, 這樣太麻煩了,族長(zhǎng)你想想,如果我要訪問(wèn)多個(gè)不同源的系統(tǒng),要是都通過(guò)book.com中轉(zhuǎn),該多麻煩!”
族長(zhǎng)想了想說(shuō):“你說(shuō)得有一定道理,我給你出個(gè)主意,既然服務(wù)器(domain)之間是互信的,那一個(gè)服務(wù)器(domain)可以設(shè)置一個(gè)白名單,里邊列出它允許哪些服務(wù)器(domain)的AJAX請(qǐng)求。假設(shè)movie.com的白名單中有book.com, 那當(dāng)屬于book.com的JavaScript試圖訪問(wèn)movie.com的時(shí)候......”
JavaScript馬上接口說(shuō):“ 這時(shí)候,我們?yōu)g覽器做點(diǎn)手腳,悄悄地把當(dāng)前的源(book.com)發(fā)過(guò)去,詢問(wèn)下movie.com, 看看他是否允許我們?cè)L問(wèn),如何允許,你們就繼續(xù)訪問(wèn),否則就報(bào)錯(cuò)!”
組長(zhǎng)說(shuō):“就是這個(gè)意思,這樣以來(lái),那些黑客就沒(méi)有辦法假冒用戶向這些互信的服務(wù)器發(fā)送請(qǐng)求了, 我把這個(gè)方法叫做Cross Origin Resource Sharing,簡(jiǎn)稱CORS,只不過(guò)這個(gè)方法需要服務(wù)器的配合了”
JavaScript表示同意,這也算是一個(gè)不錯(cuò)的妥協(xié)方法了。
【本文為51CTO專欄作者“劉欣”的原創(chuàng)稿件,轉(zhuǎn)載請(qǐng)通過(guò)作者微信公眾號(hào)coderising獲取授權(quán)】