Tornado Web服务器从设计之初就在安全方面有了很多考虑,使其能够更容易地防范那些常见的漏洞。安全 cookies 防止用户的本地状态被其浏览器中的恶意代码暗中修改。此外,浏览器cookies 可以与 HTTP 请求参数值作比较来防范跨站请求伪造攻击。
Cookie 漏洞:
许多网站使用浏览器 cookies 来存储浏览器会话间的用户标识。这是一个简单而又被广
泛兼容的方式来存储跨浏览器会话的持久状态。不幸的是,浏览器 cookies 容易受到一
些常见的攻击。
泛兼容的方式来存储跨浏览器会话的持久状态。不幸的是,浏览器 cookies 容易受到一
些常见的攻击。
Cookie 伪造:有很多方式可以在浏览器中截获 cookies。JavaScript 和 Flash 对于它们所执行的页面
的域有读写 cookies 的权限。浏览器插件也可由编程方法访问这些数据。跨站脚本攻击
可以利用这些访问来修改访客浏览器中 cookies 的值。
的域有读写 cookies 的权限。浏览器插件也可由编程方法访问这些数据。跨站脚本攻击
可以利用这些访问来修改访客浏览器中 cookies 的值。
安全 Cookies:Tornado 的安全 cookies 使用加密签名来验证 cookies 的值没有被服务器软件以外的任
何人修改过。因为一个恶意脚本并不知道安全密钥,所以它不能在应用不知情时修改
cookies。
何人修改过。因为一个恶意脚本并不知道安全密钥,所以它不能在应用不知情时修改
cookies。
使用安全 Cookies:Tornado 的 set_secure_cookie()和 get_secure_cookie()函数发送和取得浏览器
的 cookies,以防范浏览器中的恶意修改。为了使用这些函数,你必须在应用的构造函数(settings)中指定 cookie_secret (cookie的安全密钥)参数。
的 cookies,以防范浏览器中的恶意修改。为了使用这些函数,你必须在应用的构造函数(settings)中指定 cookie_secret (cookie的安全密钥)参数。
在
调用set_secure_cookie()写cookie时,会用settings中设置的cookie_secret安全密钥来对cookie进行签
名,当调用get_secure_cookie()时就会用cookie_secret安全密钥对取出的cookie值进行验证,若cookie时间戳汰
旧,或者签名与期望值不匹配则认为cookie已遭篡改,则get_secure_cookie()返回None,否则则返回cookie的值。
传递给 Application 构造函数的 cookie_secret 值应该是唯一的随机字符串。在 Python shell
下执行下面的代码片段将产生一个你自己的值:
>>> import base64, uuid
>>> base64.b64encode(uuid.uuid4().bytes + uuid.uuid4().bytes)
'bZJc2sWbQLKos6GkHn/VB9oXwQt8S0R0kRvJ5/xJ89E='
然而,Tornado 的安全 cookies 仍然容易被窃听。攻击者可能会通过脚本或浏览器插件
截获 cookies,或者干脆窃听未加密的网络数据。记住 cookie 值是签名的而不是加密的。
恶意程序能够读取已存储的 cookies,并且可以传输他们的数据到任意服务器,或者通
过发送没有修改的数据给应用伪造请求。因此,避免在浏览器 cookie 中存储敏感的用
户数据是非常重要的。
下执行下面的代码片段将产生一个你自己的值:
>>> import base64, uuid
>>> base64.b64encode(uuid.uuid4().bytes + uuid.uuid4().bytes)
'bZJc2sWbQLKos6GkHn/VB9oXwQt8S0R0kRvJ5/xJ89E='
然而,Tornado 的安全 cookies 仍然容易被窃听。攻击者可能会通过脚本或浏览器插件
截获 cookies,或者干脆窃听未加密的网络数据。记住 cookie 值是签名的而不是加密的。
恶意程序能够读取已存储的 cookies,并且可以传输他们的数据到任意服务器,或者通
过发送没有修改的数据给应用伪造请求。因此,避免在浏览器 cookie 中存储敏感的用
户数据是非常重要的。
我们还需要注意用户可能修改他自己的 cookies 的可能性,这会导致提权攻击。比如,
如果我们在 cookie 中存储了用户已付费的文章剩余的浏览数,我们希望防止用户自己
更新其中的数值来获取免费的内容。httponly 和 secure 属性可以帮助我们防范这种
攻击。
如果我们在 cookie 中存储了用户已付费的文章剩余的浏览数,我们希望防止用户自己
更新其中的数值来获取免费的内容。httponly 和 secure 属性可以帮助我们防范这种
攻击。
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_cookie 和 set_secure_cookie 方法传递关键字参
数。比如,一个安全的 HTTP-only cookie(不是 Tornado 的签名 cookie)可以调用
self.set_cookie('foo', 'bar', httponly=True, secure=True)发送。
提供的一些安全功能。这些安全属性是 HTTP cookie 规范的一部分,并在它可能是如
何暴露其值给它连接的服务器和它运行的脚本方面给予浏览器指导。比如,我们可以通
过只允许 SSL 连接的方式减少 cookie 值在网络中被截获的可能性。我们也可以让浏览
器对 JavaScript 隐藏 cookie 值。
为 cookie 设置 secure 属性来指示浏览器只通过 SSL 连接传递 cookie。(这可能会产
生一些困扰,但这不是 Tornado 的安全 cookies,更精确的说那种方法应该被称为签名
cookies。)从 Python 2.6 版本开始,Cookie 对象还提供了一个 httponly 属性。包括
这个属性指示浏览器对于 JavaScript 不可访问 cookie,这可以防范来自读取 cookie 值
的跨站脚本攻击。
为了开启这些功能,你可以向 set_cookie 和 set_secure_cookie 方法传递关键字参
数。比如,一个安全的 HTTP-only cookie(不是 Tornado 的签名 cookie)可以调用
self.set_cookie('foo', 'bar', httponly=True, secure=True)发送。
请求漏洞
任何 Web 应用所面临的一个主要安全漏洞是跨站请求伪造,通常被简写为 CSRF 或
XSRF,发音为"sea surf"。这个漏洞利用了浏览器的一个允许恶意攻击者在受害者网站
注入脚本使未授权请求代表一个已登录用户的安全漏洞。让我们看一个例子。
XSRF,发音为"sea surf"。这个漏洞利用了浏览器的一个允许恶意攻击者在受害者网站
注入脚本使未授权请求代表一个已登录用户的安全漏洞。让我们看一个例子。
防范请求伪造:
有很多预防措施可以防止这种类型的攻击。首先你在开发应用时需要深谋远虑。任何会
产生副作用的 HTTP 请求,比如点击购买按钮、编辑账户设置、改变密码或删除文档,
都应该使用 HTTP POST 方法。无论如何,这是良好的 RESTful 做法,但它也有额外的
优势用于防范像我们刚才看到的恶意图像那样琐碎的 XSRF 攻击。但是,这并不足够:
一个恶意站点可能会通过其他手段, HTML 表单或 XMLHTTPRequest API 来向你的应用发送 POST 请求。保护 POST 请求需要额外的策略。
产生副作用的 HTTP 请求,比如点击购买按钮、编辑账户设置、改变密码或删除文档,
都应该使用 HTTP POST 方法。无论如何,这是良好的 RESTful 做法,但它也有额外的
优势用于防范像我们刚才看到的恶意图像那样琐碎的 XSRF 攻击。但是,这并不足够:
一个恶意站点可能会通过其他手段, HTML 表单或 XMLHTTPRequest API 来向你的应用发送 POST 请求。保护 POST 请求需要额外的策略。
为了防范伪造 POST 请求,我们会要求每个请求包括一个参数值作为令牌来匹配存储在
cookie 中的对应值。我们的应用将通过一个 cookie 头和一个隐藏的 HTML 表单元素向
页面提供令牌。当一个合法页面的表单被提交时,它将包括表单值和已存储的 cookie。
如果两者匹配,我们的应用认定请求有效。
由于第三方站点没有访问 cookie 数据的权限,他们将不能在请求中包含令牌 cookie。
这有效地防止了不可信网站发送未授权的请求。正如我们看到的,Tornado 同样会让这
个实现变得简单。
cookie 中的对应值。我们的应用将通过一个 cookie 头和一个隐藏的 HTML 表单元素向
页面提供令牌。当一个合法页面的表单被提交时,它将包括表单值和已存储的 cookie。
如果两者匹配,我们的应用认定请求有效。
由于第三方站点没有访问 cookie 数据的权限,他们将不能在请求中包含令牌 cookie。
这有效地防止了不可信网站发送未授权的请求。正如我们看到的,Tornado 同样会让这
个实现变得简单。
使用 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 值的 POST、
PUT 和 DELETE 请求。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>
你可以通过在应用的构造函数中包含 xsrf_cookies 参数来开启 XSRF 保护:
settings = {
"cookie_secret": "bZJc2sWbQLKos6GkHn/VB9oXwQt8S0R0kRvJ5/xJ89E=",
"xsrf_cookies": True
}
application = tornado.web.Application([
(r'/', MainHandler),
(r'/purchase', PurchaseHandler),
], **settings)
当这个应用标识被设置时,
Tornado 将拒绝请求参数中不包含正确的_xsrf 值的 POST、
PUT 和 DELETE 请求。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>
开
启了XSRF防护后,Tornado对所有的POST请求默认是不信任的,所以当提交post表单是必须包含一个token,{% raw
xsrf_form_html()
%}这个标签会获取cookie中的_xsrf的cookie值,并且在html标签中包含cookie的头信息,及用hidden表单提交token值
到服务器,服务器用这个token值与cookie中存储的对应值做比较,若匹配则说明post请求是可信任的,否则则不可信任。
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 的安全编程指南。
但不是必须显式地在渲染页面时包含一个_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 的安全编程指南。
用户验证:
在
需要认证用户才允许访问的处理函数上使用@tornado.web.authenticated可以在调用之前先验证用户是否已登陆认证,若未登陆,则跳
转到settings中设置的login_url(渲染登陆页面),并且会附加上一个next参数,值为我们当前想要访问的url
示例:欢迎回来¶
在这个例子中,我们将只通过存储在安全cookie里的用户名标识一个人。当某人首次在某个浏览器(或cookie过期后)访问我们的页面时,我们展示一个登录表单页面。表单作为到LoginHandler路由的POST请求被提交。post方法的主体调用set_secure_cookie()来存储username请求参数中提交的值。
代码清单6-2中的Tornado应用展示了我们本节要讨论的验证函数。LoginHandler类渲染登录表单并设置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是应用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>