第六章:編寫安全應用

不少時候,安全應用是以犧牲複雜度(以及開發者的頭痛)爲代價的。Tornado Web服務器從設計之初就在安全方面有了不少考慮,使其可以更容易地防範那些常見的漏洞。安全cookies防止用戶的本地狀態被其瀏覽器中的惡意代碼暗中修改。此外,瀏覽器cookies能夠與HTTP請求參數值做比較來防範跨站請求僞造攻擊。在本章中,咱們將看到使防範這些漏洞更簡單的Tornado功能,以及使用這些功能的一個用戶驗證示例。html

6.1 Cookie漏洞

許多網站使用瀏覽器cookies來存儲瀏覽器會話間的用戶標識。這是一個簡單而又被普遍兼容的方式來存儲跨瀏覽器會話的持久狀態。不幸的是,瀏覽器cookies容易受到一些常見的攻擊。本節將展現Tornado是如何防止一個惡意腳原本篡改你應用存儲的cookies的。python

6.1.1 Cookie僞造

有不少方式能夠在瀏覽器中截獲cookies。JavaScript和Flash對於它們所執行的頁面的域有讀寫cookies的權限。瀏覽器插件也可由編程方法訪問這些數據。跨站腳本攻擊能夠利用這些訪問來修改訪客瀏覽器中cookies的值。web

6.1.2 安全Cookies

Tornado的安全cookies使用加密簽名來驗證cookies的值沒有被服務器軟件之外的任何人修改過。由於一個惡意腳本並不知道安全密鑰,因此它不能在應用不知情時修改cookies。ajax

6.1.2.1 使用安全Cookies

Tornado的set_secure_cookie()get_secure_cookie()函數發送和取得瀏覽器的cookies,以防範瀏覽器中的惡意修改。爲了使用這些函數,你必須在應用的構造函數中指定cookie_secret參數。讓咱們來看一個簡單的例子。shell

代碼清單6-1中的應用將渲染一個統計瀏覽器中頁面被加載次數的頁面。若是沒有設置cookie(或者cookie已經被篡改了),應用將設置一個值爲1的新cookie。不然,應用將從cookie中讀到的值加1。數據庫

代碼清單6-1 安全Cookie示例:cookie_counter.py
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值,會發現count儲存的值相似於MQ==|1310335926|8ef174ecc489ea963c5cdc26ab6d41b49502f2e2。Tornado將cookie值編碼爲Base-64字符串,並添加了一個時間戳和一個cookie內容的HMAC簽名。若是cookie的時間戳太舊(或來自將來),或簽名和指望值不匹配,get_secure_cookie()函數會認爲cookie已經被篡改,並返回None,就好像cookie從沒設置過同樣。編程

傳遞給Application構造函數的cookie_secret值應該是惟一的隨機字符串。在Python shell下執行下面的代碼片斷將產生一個你本身的值:json

>>> import base64, uuid
>>> base64.b64encode(uuid.uuid4().bytes + uuid.uuid4().bytes)
'bZJc2sWbQLKos6GkHn/VB9oXwQt8S0R0kRvJ5/xJ89E='

然而,Tornado的安全cookies仍然容易被竊聽。攻擊者可能會經過腳本或瀏覽器插件截獲cookies,或者乾脆竊聽未加密的網絡數據。記住cookie值是簽名的而不是加密的。惡意程序可以讀取已存儲的cookies,而且能夠傳輸他們的數據到任意服務器,或者經過發送沒有修改的數據給應用僞造請求。所以,避免在瀏覽器cookie中存儲敏感的用戶數據是很是重要的。瀏覽器

咱們還須要注意用戶可能修改他本身的cookies的可能性,這會致使提權攻擊。好比,若是咱們在cookie中存儲了用戶已付費的文章剩餘的瀏覽數,咱們但願防止用戶本身更新其中的數值來獲取免費的內容。httponlysecure屬性能夠幫助咱們防範這種攻擊。安全

6.1.2.2 HTTP-Only和SSL Cookies

Tornado的cookie功能依附於Python內建的Cookie模塊。所以,咱們能夠利用它所提供的一些安全功能。這些安全屬性是HTTP cookie規範的一部分,並在它多是如何暴露其值給它鏈接的服務器和它運行的腳本方面給予瀏覽器指導。好比,咱們能夠經過只容許SSL鏈接的方式減小cookie值在網絡中被截獲的可能性。咱們也可讓瀏覽器對JavaScript隱藏cookie值。

爲cookie設置secure屬性來指示瀏覽器只經過SSL鏈接傳遞cookie。(這可能會產生一些困擾,但這不是Tornado的安全cookies,更精確的說那種方法應該被稱爲簽名cookies。)從Python 2.6版本開始,Cookie對象還提供了一個httponly屬性。包括這個屬性指示瀏覽器對於JavaScript不可訪問cookie,這能夠防範來自讀取cookie值的跨站腳本攻擊。

爲了開啓這些功能,你能夠向set_cookieset_secure_cookie方法傳遞關鍵字參數。好比,一個安全的HTTP-only cookie(不是Tornado的簽名cookie)能夠調用self.set_cookie('foo', 'bar', httponly=True, secure=True)發送。

既然咱們已經探討了一些保護存儲在cookies中的持久數據的策略,下面讓咱們看看另外一種常見的攻擊載體。下一節咱們將看到一種防範向你的應用發送僞造請求的惡意網站。

6.2 請求漏洞

任何Web應用所面臨的一個主要安全漏洞是跨站請求僞造,一般被簡寫爲CSRF或XSRF,發音爲"sea surf"。這個漏洞利用了瀏覽器的一個容許惡意攻擊者在受害者網站注入腳本使未受權請求表明一個已登陸用戶的安全漏洞。讓咱們看一個例子。

6.2.1 剖析一個XSRF

假設Alice是Burt's Books的一個普通顧客。當她在這個在線商店登陸賬號後,網站使用一個瀏覽器cookie標識她。如今假設一個不擇手段的做者,Melvin,想增長他圖書的銷量。在一個Alice常常訪問的Web論壇中,他發表了一個帶有HTML圖像標籤的條目,其源碼初始化爲在線商店購物的URL。好比:

<img src="http://store.burts-books.com/purchase?title=Melvins+Web+Sploitz" />

Alice的瀏覽器嘗試獲取這個圖像資源,而且在請求中包含一個合法的cookies,並不知道取代小貓照片的是在線商店的購物URL。

6.2.2 防範請求僞造

有不少預防措施能夠防止這種類型的攻擊。首先你在開發應用時須要深謀遠慮。任何會產生反作用的HTTP請求,好比點擊購買按鈕、編輯帳戶設置、改變密碼或刪除文檔,都應該使用HTTP POST方法。不管如何,這是良好的RESTful作法,但它也有額外的優點用於防範像咱們剛纔看到的惡意圖像那樣瑣碎的XSRF攻擊。可是,這並不足夠:一個惡意站點可能會經過其餘手段,如HTML表單或XMLHTTPRequest API來向你的應用發送POST請求。保護POST請求須要額外的策略。

爲了防範僞造POST請求,咱們會要求每一個請求包括一個參數值做爲令牌來匹配存儲在cookie中的對應值。咱們的應用將經過一個cookie頭和一個隱藏的HTML表單元素向頁面提供令牌。當一個合法頁面的表單被提交時,它將包括表單值和已存儲的cookie。若是二者匹配,咱們的應用認定請求有效。

因爲第三方站點沒有訪問cookie數據的權限,他們將不能在請求中包含令牌cookie。這有效地防止了不可信網站發送未受權的請求。正如咱們看到的,Tornado一樣會讓這個實現變得簡單。

6.2.3 使用Tornado的XSRF保護

你能夠經過在應用的構造函數中包含xsrf_cookies參數來開啓XSRF保護:

settings = {
    "cookie_secret": "bZJc2sWbQLKos6GkHn/VB9oXwQt8S0R0kRvJ5/xJ89E=",
    "xsrf_cookies": True
}
application = tornado.web.Application([
    (r'/', MainHandler),
    (r'/purchase', PurchaseHandler),
], **settings)

當這個應用標識被設置時,Tornado將拒絕請求參數中不包含正確的_xsrf值的POSTPUTDELETE請求。Tornado將會在幕後處理_xsrf cookies,但你必須在你的HTML表單中包含XSRF令牌以確保受權合法請求。要作到這一點,只須要在你的模板中包含一個xsrf_form_html調用便可:

<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>

6.2.3.1 XSRF令牌和AJAX請求

AJAX請求也須要一個_xsrf參數,但不是必須顯式地在渲染頁面時包含一個_xsrf值,而是經過腳本在客戶端查詢瀏覽器得到cookie值。下面的兩個函數透明地添加令牌值給AJAX POST請求。第一個函數經過名字獲取cookie,而第二個函數是一個添加_xsrf參數到傳遞給postJSON函數數據對象的便捷函數。

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
    });
}

這些預防措施須要思考不少,而Tornado的安全cookies支持和XSRF保護減輕了應用開發者的一些負擔。能夠確定的是,內建的安全功能也很是有用,但在思考你應用的安全性方面須要時刻保持警戒。有不少在線Web應用安全文獻,其中一個更全面的實踐對策集合是Mozilla的安全編程指南

6.3 用戶驗證

既然咱們已經看到了如何安全地設置和取得cookies,並理解了XSRF攻擊背後的原理,如今就讓咱們看一個簡單用戶驗證系統的演示示例。在本節中,咱們將創建一個應用,詢問訪客的名字,而後將其存儲在安全cookie中,以便以後取出。後續的請求將認出回客,並展現給她一個定製的頁面。你將學到login_url參數和tornado.web.authenticated裝飾器的相關知識,這將消除在相似應用中常常會涉及到的一些頭疼的問題。

6.3.1 示例:歡迎回來

在這個例子中,咱們將只經過存儲在安全cookie裏的用戶名標識一我的。當某人首次在某個瀏覽器(或cookie過時後)訪問咱們的頁面時,咱們展現一個登陸表單頁面。表單做爲到LoginHandler路由的POST請求被提交。post方法的主體調用set_secure_cookie()來存儲username請求參數中提交的值。

代碼清單6-2中的Tornado應用展現了咱們本節要討論的驗證函數。LoginHandler類渲染登陸表單並設置cookie,而LogoutHandler類刪除cookie。

代碼清單6-2 驗證訪客:cookies.py
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是應用templates/目錄下的文件。

代碼清單6-3 登陸表單:login.html
<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>

 

代碼清單6-4 歡迎回客:index.html
<html>
    <head>
        <title>Welcome Back!</title>
    </head>
    <body>
        <h1>Welcome back, {{ user }}</h1>
    </body>
</html>

6.3.2 authenticated裝飾器

爲了使用Tornado的認證功能,咱們須要對登陸用戶標記具體的處理函數。咱們可使用@tornado.web.authenticated裝飾器完成它。當咱們使用這個裝飾器包裹一個處理方法時,Tornado將確保這個方法的主體只有在合法的用戶被發現時纔會調用。讓咱們看看例子中的WelcomeHandler吧,這個類只對已登陸用戶渲染index.html模板。

class WelcomeHandler(BaseHandler):
    @tornado.web.authenticated
    def get(self):
        self.render('index.html', user=self.current_user)

get方法被調用以前,authenticated裝飾器確保current_usr屬性有值。(咱們將簡短的討論這個屬性。)若是current_user值爲假(NoneFalse0""),任何GETHEAD請求都將把訪客重定向到應用設置中login_url指定的URL。此外,非法用戶的POST請求將返回一個帶有403(Forbidden)狀態的HTTP響應。

若是發現了一個合法的用戶,Tornado將如期調用處理方法。爲了實現完整功能,authenticated裝飾器依賴於current_user屬性和login_url設置,咱們將在下面看到具體講解。

6.3.2.1 current_user屬性

請求處理類有一個current_user屬性(一樣也在處理程序渲染的任何模板中可用)能夠用來存儲爲當前請求進行用戶驗證的標識。其默認值爲None。爲了authenticated裝飾器可以成功標識一個已認證用戶,你必須覆寫請求處理程序中默認的get_current_user()方法來返回當前用戶。

實際的實現由你決定,不過在這個例子中,咱們只是從安全cookie中取出訪客的姓名。很明顯,你但願使用一個更加魯棒的技術,可是出於演示的目的,咱們將使用下面的方法:

class BaseHandler(tornado.web.RequestHandler):
    def get_current_user(self):
        return self.get_secure_cookie("username")

儘管這裏討論的例子並無在存儲和取出用戶密碼或其餘憑證上有所深刻,但本章中討論的技術能夠以最小的額外努力來擴展到查詢數據庫中的認證。

6.3.2.2 login_url設置

讓咱們簡單看看應用的構造函數。記住這裏咱們傳遞了一個新的設置給應用:login_url是應用登陸表單的地址。若是get_current_user方法返回了一個假值,帶有authenticated裝飾器的處理程序將重定向瀏覽器的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)

當Tornado構建重定向URL時,它還會給查詢字符串添加一個next參數,其中包含了發起重定向到登陸頁面的URL資源地址。你可使用像self.redirect(self.get_argument('next', '/'))這樣的行來重定向登陸後用戶回到的頁面。

相關文章
相關標籤/搜索