Tornado解决跨站请求伪造XSRF

2014-07-12

Tornado是一个基于python的非阻塞式web框架,具有很好的性能。

XSRF又叫做CSRF,全称为Cross-site request forgery,即跨站请求伪造。关于XSRF,浅谈CSRF攻击方式给出了三个极好的示例。下面是地一个示例:

银行网站A,它以GET请求来完成银行转账的操作,如:http://www.mybank.com/Transfer.php?toBankId=11&money=1000

危险网站B,它里面有一段HTML的代码如下:
  <img src=http://www.mybank.com/Transfer.php?toBankId=11&money=1000>

首先,你登录了银行网站A,然后访问危险网站B,噢,这时你会发现你的银行账户少了1000块......

为什么会这样呢?原因是银行网站A违反了HTTP规范,使用GET请求更新资源。在访问危险网站B的之前,
你已经登录了银行网站A,而B中的<img>以GET的方式请求第三方资源(这里的第三方就是指银行网站了,
原本这是一个合法的请求,但这里被不法分子利用了),所以你的浏览器会带上你的银行网站A的Cookie
发出Get请求,去获取资源“http://www.mybank.com/Transfer.php?toBankId=11&money=1000”,
结果银行网站服务器收到请求后,认为这是一个更新资源操作(转账操作),所以就立刻进行转账操作。

值得注意的是,即使使用POST更新数据,仍然可以进行XSRF攻击,比如可以利用iframe。浅谈CSRF攻击方式中的示例3给出了一个使用iframe的XSRF攻击。下面,循序渐进地讲一下如何使用Tornado预防xsrf攻击。

Tornado安装


sudo pip install unittest2
sudo pip install futures
sudo pip install pycurl
sudo pip install twisted
sudo pip install pycares
sudo pip install monotime
sudo pip install tornado  #笔者安装的版本是3.2.2

简单的Hello World


编写server.py:

import tornado.ioloop
import tornado.web

class MainHandler(tornado.web.RequestHandler):
    def get(self):
        self.write("Hello, world")

application = tornado.web.Application([
    (r"/", MainHandler),
])

if __name__ == "__main__":
    application.listen(8888)
    tornado.ioloop.IOLoop.instance().start()

运行server.py,浏览器访问http://127.0.0.1:8888/,浏览器会显示Hello, world

使用模板


将server.py改写为:

import tornado.ioloop
import tornado.web

class MainHandler(tornado.web.RequestHandler):
    def get(self):
        items = ["Item 1", "Item 2", "Item 3"]
        self.render("template.html", title="My title", items=items)

application = tornado.web.Application([
    (r"/", MainHandler),
])

if __name__ == "__main__":
    application.listen(8888)
    tornado.ioloop.IOLoop.instance().start()

在同样的目录中创建模板文件template.html:

<html>
   <head>
      <title>{{ title }}</title>
   </head>
   <body>
     <ul>
       {% for item in items %}
         <li>{{ escape(item) }}</li>
       {% end %}
     </ul>
   </body>
</html>

访问效果如下:

如何防止XSRF攻击


将server.py改写为:

import tornado.ioloop
import tornado.web

settings = {
    "cookie_secret": "afalihqifacn7q!@as",
    "xsrf_cookies": True,
}

class MainHandler(tornado.web.RequestHandler):
    def get(self):
        self.render("index.html")

class LoginHandler(tornado.web.RequestHandler):
    def post(self):
        self.write("hello")

router = [(r"/", MainHandler),
          (r"/login", LoginHandler)]

application = tornado.web.Application(router, **settings)

if __name__ == "__main__":
    application.listen(8888)
    tornado.ioloop.IOLoop.instance().start()

在同一目录下创建模板文件index.html:

<html>
   <head>
      <title></title>
   </head>
   <body>

    <form action="/login" method="post">
      {% raw xsrf_form_html() %}
      <input type="text" name="message"/>
      <input type="submit" value="Post1"/>
    </form>

    <form action="/login" method="post">
      <input type="text" name="message"/>
      <input type="submit" value="Post2"/>
    </form>

   </body>
</html>

运行server.py,现在使用浏览器打开http://127.0.0.1:8888/,会看到:

我们通过"xsrf_cookies": True启用了预防xsrf攻击的功能。第一个表单可以正常跳转到/login。下面根据访问过程论述一下预防的原理:

首先,访问http://127.0.0.1:8888/时,http请求头类似于下面的内容:

GET / HTTP/1.1
Host: 127.0.0.1:8888
Connection: keep-alive
Cache-Control: max-age=0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
User-Agent: Mozilla/5.0 (X11; Linux x86_64) 
Accept-Encoding: gzip,deflate,sdch

服务器server.py响应头如下:

HTTP/1.1 200 OK
Date: Sat, 12 Jul 2014 05:48:21 GMT
Content-Length: 395
Etag: "c8a35073b1b6af47460c7318c8e9069ef998615d"
Content-Type: text/html; charset=UTF-8
Server: TornadoServer/3.2.2
Set-Cookie: _xsrf=2|fbe9aed0|553cc0b9bb7f3d7de9271add4199c185|1405144101; Path=/

在最后一行可以看到,服务器需要对客户端设置键为_xsrf的cookie,值得注意的是每次请求得到的_xsrf的值是变化的。

响应主体是html:

<html>
<head>
<title></title>
</head>
<body>

<form action="/login" method="post">
<input type="hidden" name="_xsrf" value="2|fbe9aed0|553cc0b9bb7f3d7de9271add4199c185|1405144101"/>
<input type="text" name="message"/>
<input type="submit" value="Post1"/>
</form>

<form action="/login" method="post">
<input type="text" name="message"/>
<input type="submit" value="Post2"/>
</form>

</body>
</html>

注意到第一个表单的的第一个input是隐藏的,且其value和响应头cookie中设置的_xsrf的值是相同的。

点击表单1的按钮后,表单中_xsrfmessage的值会以post的方式传到http://127.0.0.1:8888/login,http请求头中也包含了之前设置的cookie(即_xsrf)。server.py会对这两个_xsrf进行比较,相同的话则意味着不是XSRF攻击,浏览器会显示hello

如果点击表格2的按钮Post2,由于两个_xsrf不相同(其实表单中就没有_xsrf),得到的响应会是:

403: Forbidden

如果在其他的域下设法向http://127.0.0.1:8888/loginPOST数据,由于cookie中没有_xsrf或者没有正确的_xsrf,POST将无法成功。这也就防范了XSRF攻击。

一些资料


http://www.tornadoweb.cn/
Tornado概览
Tornado Documentation
Introduction to Tornado——中文版

( 完 )