很多時(shí)候,安全應(yīng)用是以犧牲復(fù)雜度(以及開發(fā)者的頭痛)為代價(jià)的。Tornado Web服務(wù)器從設(shè)計(jì)之初就在安全方面有了很多考慮,使其能夠更容易地防范那些常見的漏洞。安全cookies防止用戶的本地狀態(tài)被其瀏覽器中的惡意代碼暗中修改。此外,瀏覽器cookies可以與HTTP請(qǐng)求參數(shù)值作比較來(lái)防范跨站請(qǐng)求偽造攻擊。在本章中,我們將看到使防范這些漏洞更簡(jiǎn)單的Tornado功能,以及使用這些功能的一個(gè)用戶驗(yàn)證示例。
許多網(wǎng)站使用瀏覽器cookies來(lái)存儲(chǔ)瀏覽器會(huì)話間的用戶標(biāo)識(shí)。這是一個(gè)簡(jiǎn)單而又被廣泛兼容的方式來(lái)存儲(chǔ)跨瀏覽器會(huì)話的持久狀態(tài)。不幸的是,瀏覽器cookies容易受到一些常見的攻擊。本節(jié)將展示Tornado是如何防止一個(gè)惡意腳本來(lái)篡改你應(yīng)用存儲(chǔ)的cookies的。
有很多方式可以在瀏覽器中截獲cookies。JavaScript和Flash對(duì)于它們所執(zhí)行的頁(yè)面的域有讀寫cookies的權(quán)限。瀏覽器插件也可由編程方法訪問這些數(shù)據(jù)??缯灸_本攻擊可以利用這些訪問來(lái)修改訪客瀏覽器中cookies的值。
Tornado的安全cookies使用加密簽名來(lái)驗(yàn)證cookies的值沒有被服務(wù)器軟件以外的任何人修改過(guò)。因?yàn)橐粋€(gè)惡意腳本并不知道安全密鑰,所以它不能在應(yīng)用不知情時(shí)修改cookies。
Tornado的set_secure_cookie()和get_secure_cookie()函數(shù)發(fā)送和取得瀏覽器的cookies,以防范瀏覽器中的惡意修改。為了使用這些函數(shù),你必須在應(yīng)用的構(gòu)造函數(shù)中指定cookie_secret參數(shù)。讓我們來(lái)看一個(gè)簡(jiǎn)單的例子。
代碼清單6-1中的應(yīng)用將渲染一個(gè)統(tǒng)計(jì)瀏覽器中頁(yè)面被加載次數(shù)的頁(yè)面。如果沒有設(shè)置cookie(或者cookie已經(jīng)被篡改了),應(yīng)用將設(shè)置一個(gè)值為1的新cookie。否則,應(yīng)用將從cookie中讀到的值加1。
import tornado.httpserver import tornado.ioloop import tornado.web import tornado.options from tornado.options import define, options define("port", default=8000, help="run on the given port", type=int) class MainHandler(tornado.web.RequestHandler): def get(self): cookie = self.get_secure_cookie("count") count = int(cookie) + 1 if cookie else 1 countString = "1 time" if count == 1 else "%d times" % count self.set_secure_cookie("count", str(count)) self.write( '<html><head><title>Cookie Counter</title></head>' '<body><h1>You’ve viewed this page %s times.</h1>' % countString + '</body></html>' ) if __name__ == "__main__": tornado.options.parse_command_line() settings = { "cookie_secret": "bZJc2sWbQLKos6GkHn/VB9oXwQt8S0R0kRvJ5/xJ89E=" } application = tornado.web.Application([ (r'/', MainHandler) ], **settings) http_server = tornado.httpserver.HTTPServer(application) http_server.listen(options.port) tornado.ioloop.IOLoop.instance().start()
如果你檢查瀏覽器中的cookie值,會(huì)發(fā)現(xiàn)count儲(chǔ)存的值類似于MQ==|1310335926|8ef174ecc489ea963c5cdc26ab6d41b49502f2e2。Tornado將cookie值編碼為Base-64字符串,并添加了一個(gè)時(shí)間戳和一個(gè)cookie內(nèi)容的HMAC簽名。如果cookie的時(shí)間戳太舊(或來(lái)自未來(lái)),或簽名和期望值不匹配,get_secure_cookie()函數(shù)會(huì)認(rèn)為cookie已經(jīng)被篡改,并返回None,就好像cookie從沒設(shè)置過(guò)一樣。
傳遞給Application構(gòu)造函數(shù)的cookie_secret值應(yīng)該是唯一的隨機(jī)字符串。在Python shell下執(zhí)行下面的代碼片段將產(chǎn)生一個(gè)你自己的值:
>>> import base64, uuid >>> base64.b64encode(uuid.uuid4().bytes + uuid.uuid4().bytes) 'bZJc2sWbQLKos6GkHn/VB9oXwQt8S0R0kRvJ5/xJ89E='
然而,Tornado的安全cookies仍然容易被竊聽。攻擊者可能會(huì)通過(guò)腳本或?yàn)g覽器插件截獲cookies,或者干脆竊聽未加密的網(wǎng)絡(luò)數(shù)據(jù)。記住cookie值是簽名的而不是加密的。惡意程序能夠讀取已存儲(chǔ)的cookies,并且可以傳輸他們的數(shù)據(jù)到任意服務(wù)器,或者通過(guò)發(fā)送沒有修改的數(shù)據(jù)給應(yīng)用偽造請(qǐng)求。因此,避免在瀏覽器cookie中存儲(chǔ)敏感的用戶數(shù)據(jù)是非常重要的。
我們還需要注意用戶可能修改他自己的cookies的可能性,這會(huì)導(dǎo)致提權(quán)攻擊。比如,如果我們?cè)赾ookie中存儲(chǔ)了用戶已付費(fèi)的文章剩余的瀏覽數(shù),我們希望防止用戶自己更新其中的數(shù)值來(lái)獲取免費(fèi)的內(nèi)容。httponly和secure屬性可以幫助我們防范這種攻擊。
Tornado的cookie功能依附于Python內(nèi)建的Cookie模塊。因此,我們可以利用它所提供的一些安全功能。這些安全屬性是HTTP cookie規(guī)范的一部分,并在它可能是如何暴露其值給它連接的服務(wù)器和它運(yùn)行的腳本方面給予瀏覽器指導(dǎo)。比如,我們可以通過(guò)只允許SSL連接的方式減少cookie值在網(wǎng)絡(luò)中被截獲的可能性。我們也可以讓瀏覽器對(duì)JavaScript隱藏cookie值。
為cookie設(shè)置secure屬性來(lái)指示瀏覽器只通過(guò)SSL連接傳遞cookie。(這可能會(huì)產(chǎn)生一些困擾,但這不是Tornado的安全cookies,更精確的說(shuō)那種方法應(yīng)該被稱為簽名cookies。)從Python 2.6版本開始,Cookie對(duì)象還提供了一個(gè)httponly屬性。包括這個(gè)屬性指示瀏覽器對(duì)于JavaScript不可訪問cookie,這可以防范來(lái)自讀取cookie值的跨站腳本攻擊。
為了開啟這些功能,你可以向set_cookie和set_secure_cookie方法傳遞關(guān)鍵字參數(shù)。比如,一個(gè)安全的HTTP-only cookie(不是Tornado的簽名cookie)可以調(diào)用self.set_cookie('foo', 'bar', httponly=True, secure=True)
發(fā)送。
既然我們已經(jīng)探討了一些保護(hù)存儲(chǔ)在cookies中的持久數(shù)據(jù)的策略,下面讓我們看看另一種常見的攻擊載體。下一節(jié)我們將看到一種防范向你的應(yīng)用發(fā)送偽造請(qǐng)求的惡意網(wǎng)站。
任何Web應(yīng)用所面臨的一個(gè)主要安全漏洞是跨站請(qǐng)求偽造,通常被簡(jiǎn)寫為CSRF或XSRF,發(fā)音為"sea surf"。這個(gè)漏洞利用了瀏覽器的一個(gè)允許惡意攻擊者在受害者網(wǎng)站注入腳本使未授權(quán)請(qǐng)求代表一個(gè)已登錄用戶的安全漏洞。讓我們看一個(gè)例子。
假設(shè)Alice是Burt's Books的一個(gè)普通顧客。當(dāng)她在這個(gè)在線商店登錄帳號(hào)后,網(wǎng)站使用一個(gè)瀏覽器cookie標(biāo)識(shí)她?,F(xiàn)在假設(shè)一個(gè)不擇手段的作者,Melvin,想增加他圖書的銷量。在一個(gè)Alice經(jīng)常訪問的Web論壇中,他發(fā)表了一個(gè)帶有HTML圖像標(biāo)簽的條目,其源碼初始化為在線商店購(gòu)物的URL。比如:
<img src="http://store.burts-books.com/purchase?title=Melvins+Web+Sploitz" rel="external nofollow" />
Alice的瀏覽器嘗試獲取這個(gè)圖像資源,并且在請(qǐng)求中包含一個(gè)合法的cookies,并不知道取代小貓照片的是在線商店的購(gòu)物URL。
有很多預(yù)防措施可以防止這種類型的攻擊。首先你在開發(fā)應(yīng)用時(shí)需要深謀遠(yuǎn)慮。任何會(huì)產(chǎn)生副作用的HTTP請(qǐng)求,比如點(diǎn)擊購(gòu)買按鈕、編輯賬戶設(shè)置、改變密碼或刪除文檔,都應(yīng)該使用HTTP POST方法。無(wú)論如何,這是良好的RESTful做法,但它也有額外的優(yōu)勢(shì)用于防范像我們剛才看到的惡意圖像那樣瑣碎的XSRF攻擊。但是,這并不足夠:一個(gè)惡意站點(diǎn)可能會(huì)通過(guò)其他手段,如HTML表單或XMLHTTPRequest API來(lái)向你的應(yīng)用發(fā)送POST請(qǐng)求。保護(hù)POST請(qǐng)求需要額外的策略。
為了防范偽造POST請(qǐng)求,我們會(huì)要求每個(gè)請(qǐng)求包括一個(gè)參數(shù)值作為令牌來(lái)匹配存儲(chǔ)在cookie中的對(duì)應(yīng)值。我們的應(yīng)用將通過(guò)一個(gè)cookie頭和一個(gè)隱藏的HTML表單元素向頁(yè)面提供令牌。當(dāng)一個(gè)合法頁(yè)面的表單被提交時(shí),它將包括表單值和已存儲(chǔ)的cookie。如果兩者匹配,我們的應(yīng)用認(rèn)定請(qǐng)求有效。
由于第三方站點(diǎn)沒有訪問cookie數(shù)據(jù)的權(quán)限,他們將不能在請(qǐng)求中包含令牌cookie。這有效地防止了不可信網(wǎng)站發(fā)送未授權(quán)的請(qǐng)求。正如我們看到的,Tornado同樣會(huì)讓這個(gè)實(shí)現(xiàn)變得簡(jiǎn)單。
你可以通過(guò)在應(yīng)用的構(gòu)造函數(shù)中包含xsrf_cookies參數(shù)來(lái)開啟XSRF保護(hù):
settings = { "cookie_secret": "bZJc2sWbQLKos6GkHn/VB9oXwQt8S0R0kRvJ5/xJ89E=", "xsrf_cookies": True } application = tornado.web.Application([ (r'/', MainHandler), (r'/purchase', PurchaseHandler), ], **settings)
當(dāng)這個(gè)應(yīng)用標(biāo)識(shí)被設(shè)置時(shí),Tornado將拒絕請(qǐng)求參數(shù)中不包含正確的_xsrf值的POST、PUT和DELETE請(qǐng)求。Tornado將會(huì)在幕后處理_xsrf cookies,但你必須在你的HTML表單中包含XSRF令牌以確保授權(quán)合法請(qǐng)求。要做到這一點(diǎn),只需要在你的模板中包含一個(gè)xsrf_form_html調(diào)用即可:
<form action="/purchase" method="POST"> {% raw xsrf_form_html() %} <input type="text" name="title" /> <input type="text" name="quantity" /> <input type="submit" value="Check Out" /> </form>
AJAX請(qǐng)求也需要一個(gè)_xsrf參數(shù),但不是必須顯式地在渲染頁(yè)面時(shí)包含一個(gè)_xsrf值,而是通過(guò)腳本在客戶端查詢?yōu)g覽器獲得cookie值。下面的兩個(gè)函數(shù)透明地添加令牌值給AJAX POST請(qǐng)求。第一個(gè)函數(shù)通過(guò)名字獲取cookie,而第二個(gè)函數(shù)是一個(gè)添加_xsrf參數(shù)到傳遞給postJSON函數(shù)數(shù)據(jù)對(duì)象的便捷函數(shù)。
function getCookie(name) { var c = document.cookie.match("\\b" + name + "=([^;]*)\\b"); return c ? c[1] : undefined; } jQuery.postJSON = function(url, data, callback) { data._xsrf = getCookie("_xsrf"); jQuery.ajax({ url: url, data: jQuery.param(data), dataType: "json", type: "POST", success: callback }); }
這些預(yù)防措施需要思考很多,而Tornado的安全cookies支持和XSRF保護(hù)減輕了應(yīng)用開發(fā)者的一些負(fù)擔(dān)??梢钥隙ǖ氖牵瑑?nèi)建的安全功能也非常有用,但在思考你應(yīng)用的安全性方面需要時(shí)刻保持警惕。有很多在線Web應(yīng)用安全文獻(xiàn),其中一個(gè)更全面的實(shí)踐對(duì)策集合是Mozilla的安全編程指南。
既然我們已經(jīng)看到了如何安全地設(shè)置和取得cookies,并理解了XSRF攻擊背后的原理,現(xiàn)在就讓我們看一個(gè)簡(jiǎn)單用戶驗(yàn)證系統(tǒng)的演示示例。在本節(jié)中,我們將建立一個(gè)應(yīng)用,詢問訪客的名字,然后將其存儲(chǔ)在安全cookie中,以便之后取出。后續(xù)的請(qǐng)求將認(rèn)出回客,并展示給她一個(gè)定制的頁(yè)面。你將學(xué)到login_url參數(shù)和tornado.web.authenticated裝飾器的相關(guān)知識(shí),這將消除在類似應(yīng)用中經(jīng)常會(huì)涉及到的一些頭疼的問題。
在這個(gè)例子中,我們將只通過(guò)存儲(chǔ)在安全cookie里的用戶名標(biāo)識(shí)一個(gè)人。當(dāng)某人首次在某個(gè)瀏覽器(或cookie過(guò)期后)訪問我們的頁(yè)面時(shí),我們展示一個(gè)登錄表單頁(yè)面。表單作為到LoginHandler路由的POST請(qǐng)求被提交。post方法的主體調(diào)用set_secure_cookie()來(lái)存儲(chǔ)username請(qǐng)求參數(shù)中提交的值。
代碼清單6-2中的Tornado應(yīng)用展示了我們本節(jié)要討論的驗(yàn)證函數(shù)。LoginHandler類渲染登錄表單并設(shè)置cookie,而LogoutHandler類刪除cookie。
import tornado.httpserver import tornado.ioloop import tornado.web import tornado.options import os.path from tornado.options import define, options define("port", default=8000, help="run on the given port", type=int) class BaseHandler(tornado.web.RequestHandler): def get_current_user(self): return self.get_secure_cookie("username") class LoginHandler(BaseHandler): def get(self): self.render('login.html') def post(self): self.set_secure_cookie("username", self.get_argument("username")) self.redirect("/") class WelcomeHandler(BaseHandler): @tornado.web.authenticated def get(self): self.render('index.html', user=self.current_user) class LogoutHandler(BaseHandler): def get(self): if (self.get_argument("logout", None)): self.clear_cookie("username") self.redirect("/") if __name__ == "__main__": tornado.options.parse_command_line() settings = { "template_path": os.path.join(os.path.dirname(__file__), "templates"), "cookie_secret": "bZJc2sWbQLKos6GkHn/VB9oXwQt8S0R0kRvJ5/xJ89E=", "xsrf_cookies": True, "login_url": "/login" } application = tornado.web.Application([ (r'/', WelcomeHandler), (r'/login', LoginHandler), (r'/logout', LogoutHandler) ], **settings) http_server = tornado.httpserver.HTTPServer(application) http_server.listen(options.port) tornado.ioloop.IOLoop.instance().start()
代碼清單6-3和6-4是應(yīng)用templates/目錄下的文件。
<html> <head> <title>Please Log In</title> </head> <body> <form action="/login" method="POST"> {% raw xsrf_form_html() %} Username: <input type="text" name="username" /> <input type="submit" value="Log In" /> </form> </body> </html>
<html> <head> <title>Welcome Back!</title> </head> <body> <h1>Welcome back, {{ user }}</h1> </body> </html>
為了使用Tornado的認(rèn)證功能,我們需要對(duì)登錄用戶標(biāo)記具體的處理函數(shù)。我們可以使用@tornado.web.authenticated裝飾器完成它。當(dāng)我們使用這個(gè)裝飾器包裹一個(gè)處理方法時(shí),Tornado將確保這個(gè)方法的主體只有在合法的用戶被發(fā)現(xiàn)時(shí)才會(huì)調(diào)用。讓我們看看例子中的WelcomeHandler吧,這個(gè)類只對(duì)已登錄用戶渲染index.html模板。
class WelcomeHandler(BaseHandler): @tornado.web.authenticated def get(self): self.render('index.html', user=self.current_user)
在get方法被調(diào)用之前,authenticated裝飾器確保current_usr屬性有值。(我們將簡(jiǎn)短的討論這個(gè)屬性。)如果current_user值為假(None、False、0、""),任何GET或HEAD請(qǐng)求都將把訪客重定向到應(yīng)用設(shè)置中l(wèi)ogin_url指定的URL。此外,非法用戶的POST請(qǐng)求將返回一個(gè)帶有403(Forbidden)狀態(tài)的HTTP響應(yīng)。
如果發(fā)現(xiàn)了一個(gè)合法的用戶,Tornado將如期調(diào)用處理方法。為了實(shí)現(xiàn)完整功能,authenticated裝飾器依賴于current_user屬性和login_url設(shè)置,我們將在下面看到具體講解。
請(qǐng)求處理類有一個(gè)current_user屬性(同樣也在處理程序渲染的任何模板中可用)可以用來(lái)存儲(chǔ)為當(dāng)前請(qǐng)求進(jìn)行用戶驗(yàn)證的標(biāo)識(shí)。其默認(rèn)值為None。為了authenticated裝飾器能夠成功標(biāo)識(shí)一個(gè)已認(rèn)證用戶,你必須覆寫請(qǐng)求處理程序中默認(rèn)的get_current_user()方法來(lái)返回當(dāng)前用戶。
實(shí)際的實(shí)現(xiàn)由你決定,不過(guò)在這個(gè)例子中,我們只是從安全cookie中取出訪客的姓名。很明顯,你希望使用一個(gè)更加魯棒的技術(shù),但是出于演示的目的,我們將使用下面的方法:
class BaseHandler(tornado.web.RequestHandler): def get_current_user(self): return self.get_secure_cookie("username")
盡管這里討論的例子并沒有在存儲(chǔ)和取出用戶密碼或其他憑證上有所深入,但本章中討論的技術(shù)可以以最小的額外努力來(lái)擴(kuò)展到查詢數(shù)據(jù)庫(kù)中的認(rèn)證。
讓我們簡(jiǎn)單看看應(yīng)用的構(gòu)造函數(shù)。記住這里我們傳遞了一個(gè)新的設(shè)置給應(yīng)用:login_url是應(yīng)用登錄表單的地址。如果get_current_user方法返回了一個(gè)假值,帶有authenticated裝飾器的處理程序?qū)⒅囟ㄏ驗(yàn)g覽器的URL以便登錄。
settings = { "template_path": os.path.join(os.path.dirname(__file__), "templates"), "cookie_secret": "bZJc2sWbQLKos6GkHn/VB9oXwQt8S0R0kRvJ5/xJ89E=", "xsrf_cookies": True, "login_url": "/login" } application = tornado.web.Application([ (r'/', WelcomeHandler), (r'/login', LoginHandler), (r'/logout', LogoutHandler) ], **settings)
當(dāng)Tornado構(gòu)建重定向URL時(shí),它還會(huì)給查詢字符串添加一個(gè)next參數(shù),其中包含了發(fā)起重定向到登錄頁(yè)面的URL資源地址。你可以使用像self.redirect(self.get_argument('next', '/'))這樣的行來(lái)重定向登錄后用戶回到的頁(yè)面。
我們?cè)诒菊轮锌吹搅藘煞N幫助你的Tornado應(yīng)用安全的技術(shù),以及一個(gè)如何使用@tornado.web.authenticated實(shí)現(xiàn)用戶認(rèn)證的例子。在第七章,我們將看到在那些像Facebook和Twitter一樣需要外部Web服務(wù)認(rèn)證的應(yīng)用中如何擴(kuò)展我們這里談?wù)摰母拍睢?/p>
更多建議: