前言:HTML5出现后,网络安全更加受到广泛的关注。Web对于网络安全有哪些改进?我们如何来面对越来越危险的网络欺诈和攻击?下面的文章谈到了W3C对于这个问题的最新解决方案。未来有机会的话,我会针对XSS、P3P、同源策略、CORS(跨域资源共享)和CSP进行关于HTML5内容安全的现场分享。
------------------------华丽的分界线
注意:本文所讨论的API还未最终确定,请在自己的项目中谨慎使用。
万维网的安全策略植根于同源策略。例如http://blog.csdn.net/hfahe的代码只能访问http://blog.csdn.net的数据,而没有访问http://www.baidu.com的权限。每个来源都与网络的其它部分分隔开,为开发人员构建了一个安全的沙箱。理论上这是完美的,但是现在攻击者已经找到了聪明的方式来破坏这个系统。
这就是XSS跨站脚本攻击,通过虚假内容和诱骗点击来绕过同源策略。这是一个很大的问题,如果攻击者成功注入代码,有相当多的用户数据会被泄漏。
现在我们介绍一个全新的、有效的安全防御策略来减轻这种风险,这就是内容安全策略(ContentSecurity Policy,CSP)。
来源白名单
XSS攻击的核心是利用了浏览器无法区分脚本是被第三方注入的,还是真的是你应用程序的一部分。例如Google +1按钮会从https://apis.google.com/js/plusone.js加载并执行代码,但是我们不能指望从浏览器上的图片就能判断出代码是真的来自apis.google.com,还是来自apis.evil.example.com。浏览器下载并执行任意代码的页面请求,而不论其来源。
CSP定义了Content-Security-PolicyHTTP头来允许你创建一个可信来源的白名单,使得浏览器只执行和渲染来自这些来源的资源,而不是盲目信任服务器提供的所有内容。即使攻击者可以找到漏洞来注入脚本,但是因为来源不包含在白名单里,因此将不会被执行。
以上面Google +1按钮为例,因为我们相信apis.google.com提供有效的代码,以及我们自己,所以可以定义一个策略,允许浏览器只会执行下面两个来源之一的脚本。
Content-Security-Policy:script-src 'self' https://apis.google.com
是不是很简单?script-src可以为指定页面控制脚本相关权限。这样浏览器只会下载和执行来自http://apis.google.com和本页自身的脚本。
一旦我们定义了这个策略,浏览器会在检测到注入代码时抛出一个错误(请注意是什么浏览器)。
内容安全策略适用于所有常用资源
虽然脚本资源是最明显的安全隐患,但是CSP还提供了一套丰富的指令集,允许页面控制加载各种类型的资源,例如如下的类型:
- content-src:限制连接的类型(例如XHR、WebSockets和EventSource)
- font-src:控制网络字体的来源。例如可以通过font-src https://themes.googleusercontent.com来使用Google的网络字体。
- frame-src:列出了可以嵌入的frame的来源。例如frame-src https://youtube.com只允许嵌入YouTube的视频。。
- img-src:定义了可加载图像的来源。
- media-src:限制视频和音频的来源。
- object-src:限制Flash和其他插件的来源。
- style-src:类似于Script-src,只是作用于css文件。
默认情况下,所有的设置都是打开的,不做任何限制。你可以以分号分隔多个指令,但是类似于script-srchttps://host1.com;script-src https://host2.com的形式,第二个指令将会被忽略。正确的写法是script-srchttps://host1.com https://host2.com。
例如,你有一个应用需要从内容分发网络(CDN,例如https://cdn.example.net)加载所有的资源,并且知道不需要任何frame和插件的内容,你的策略可能会像下面这样:
- Content-Security-Policy:default-src https://cdn.example.net; frame-src 'none'; object-src 'none'
细节
我在例子里使用的HTTP头是Content-Security-Policy,但是现代浏览器已经通过前缀来提供了支持:Firefox使用x-Content-Security-Policy,WebKit使用X-WebKit-CSP。未来会逐步过渡到统一的标准。
策略可以根据每个不同的页面而设定,这提供了很大的灵活度。因为你的站点可能有的页面有Google +1的按钮,而有的则没有。
每个指令的来源列表可以相当灵活,你可以指定模式(data:, https:),或者指定主机名在一个范围(example.com,它匹配主机上的任意来源、任意模式和任意端口),或者指定一个完整的URI(https://example.com:443,特指https协议,example.com域名,443端口)。
你在来源列表中还可以使用四个关键字:
- “none”:你可能期望不匹配任何内容
- “self”:与当前来源相同,但不包含子域
- “unsafe-inline”:允许内联Javascript和CSS
- “unsafe-eval”:允许文本到JS的机制例如eval
请注意,这些关键词需要加引号。
沙箱
这里还有另外一个值得讨论的指令:sandbox。和其他指令有些不一致,它主要是控制页面上采取的行为,而不是页面能够加载的资源。如果设置了这个属性,页面就表现为一个设置了sandbox属性的frame一样。这对页面有很大范围的影响,例如防止表单提交等。这有点超出了本文的范围,但是你可以在HTML5规范的“沙箱标志设置”章节找到更多信息。
有害的内联代码
CSP基于来源白名单,但是它不能解决XSS攻击的最大来源:内联脚本注入。如果攻击者可以注入包含有害代码的script标签(<script>sendMyDataToEvilDotCom();</script>),浏览器并没有好的机制来区分这个标签。CSP只能通过完全禁止内联脚本来解决这个问题。
这个禁止项不仅包括脚本中嵌入的script标签,还包括内联事件处理程序和javascrpt:这种URL。你需要把script标签的内容放到一个外部文件里,并且用适当的addEventListener的方式替换javascript:和<a… οnclick=”[JAVASCRIPT]”>。例如,你可能会把下面的表单:
- <script>
- function doAmazingThings() {
- alert('YOU AM AMAZING!');
- }
- </script>
- <button onclick='doAmazingThings();'>Am I amazing?</button>
重写为下面的形式:
无论是否使用CSP,以上的代码其实有更大的优点。内联JavaScript完全混合了结构和行为,你不应该这么做。另外外联资源更容易进行浏览器缓存,开发者更容易理解,并且便于编译和压缩。如果采用外联代码,你会写出更好的代码。
- <!-- amazing.html -->
- <script src='amazing.js'></script>
- <button id='amazing'>Am I amazing?</button>
- // amazing.js
- function doAmazingThings() {
- alert('YOU AM AMAZING!');
- }
- document.addEventListener('DOMContentReady', function () {
- document.getElementById('amazing')
- .addEventListener('click', doAmazingThings);
- });
内联样式需要以同样的方式进行处理,无论是style属性还是style标签都需要提取到外部样式表中。这样可以防止各式各样神奇的数据泄漏方式。
如果你必须要有内联脚本和样式,可以为script-src or style-src属性设定'unsafe-inline值。但是不要这样做,禁止内联脚本是CSP提供的最大安全保证,同时禁止内联样式可以让你的应用变得更加安全和健壮。这是一个权衡,但是非常值得。
Eval
即便攻击者不能直接注入脚本,他可能会诱使你的应用把插入的文本转换为可执行脚本并且自我执行。eval() , newFunction() , setTimeout([string], ...) 和setInterval([string], ...) 都可能成为这种危险的载体。CSP针对这种风险的策略是,完全阻止这些载体。
这对你构建应用的方式有一些影响:
通过内置的JSON.parse解析JSON,而不依靠eval。IE8以后的浏览器都支持本地JSON操作,这是完全安全的。
通过内联函数代替字符串来重写你setTimeout和setInterval的调用方式。例如:
- setTimeout("document.querySelector('a').style.display = 'none';", 10);
可以重写为:
避免运行时的内联模版:许多模版库都使用new Function()以加速模版的生成。这对动态程序来说非常棒,但是对恶意文本来说存在风险。
- setTimeout(function () { document.querySelector('a').style.display = 'none'; }, 10);
报告
CSP可以在服务器端阻止不可信的资源对用户来说非常有用,但是对于获取各种发送到服务器的通知来说对我们却非常有用,这样我们就能识别和修复任何恶意脚本注入。为此你可以通过report-uri指令指示浏览器发送JSON格式的拦截报告到某个地址。
- Content-Security-Policy: default-src 'self'; ...; report-uri /my_amazing_csp_report_parser;
报告看起来会像下面这样:
- {
- "csp-report": {
- "document-uri": "http://example.org/page.html",
- "referrer": "http://evil.example.com/",
- "blocked-uri": "http://evil.example.com/evil.js",
- "violated-directive": "script-src 'self' https://apis.google.com",
- "original-policy": "script-src 'self' https://apis.google.com; report-uri http://example.org/my_amazing_csp_report_parser"
- }
- }
其中包含的信息会帮助你识别拦截的情况,包括拦截发生的页面(document-uri),页面的referrer,违反页面策略的资源(blocked-uri),所违反的指令(violated-directive)以及页面所有的内容安全策略(original-policy)。
现实用法
CSP现在在Chrome 16+和Firefox 4+的浏览器上可用,并且它在IE10上预计会获得有限的支持。Safari目前还不支持,但是WebKit每晚构建版已经可用,所以预计Safari将会在下面的迭代中提供支持。
下面让我们看一些常用的用例:
实际案例1:社会化媒体widget
- Google +1 button包括来自https://apis.google.com的脚本,以及嵌入自https://plusone.google.com的iframe。你的策略需要包含这些源来使用Google +1的按钮。最简单的策略是script-srchttps://apis.google.com; frame-src https://plusone.google.com。你还需要确保Google提供的JS片段存放在外部的JS文件里。
- Facebook的Like按钮有许多种实现方案。我建议你坚持使用iframe版本,因为它可以和你站点的其它部分保持很好的隔离。这需要使用frame-src https://facebook.com指令。请注意,默认情况下,Facebook提供的iframe代码使用的是相对路径//facebook.com,请把这段代码修改为https://facebook.com,HTTP你没有必要可以不使用。
- Twitter的Tweet按钮依赖于script和frame,都来自于https://platform.twitter.com(Twitter默认提供的是相对URL,请在复制的时候编辑代码来指定为HTTPS方式)。
其它的平台有相似的情况,可以类似的解决。我建议把default-src设置为none,然后查看控制台来检查你需要使用哪些资源来确保widget正常工作。
使用多个widget非常简单:只需要合并所有的策略指令,记住把同一指令的设置都放在一起。如果你想使用上面这三个widget,策略看起来会像下面这样:
- script-src https://apis.google.com https://platform.twitter.com; frame-src https://plusone.google.com https://facebook.com https://platform.twitter.com
实际案例2:防御
假设你访问一个银行网站,并且希望确保只加载你所需的资源。在这种情况下,开始设置一个默认的权限来阻止所有的内容(default-src ‘none’),并且从这从头构建策略。
比如,银行网站需要从来自https://cdn.mybank.net的CDN加载图像、样式和脚本,并且通过XHR连接到https://api.mybank.com/来拉取各种数据,还需要使用frame,但是frame都来自非第三方的本地页面。网站上没有Flash、字体和其他内容。这种情况下我们可以发送最严格的CSP头是:
- Content-Security-Policy: default-src 'none'; script-src https://cdn.mybank.net; style-src https://cdn.mybank.net; img-src https://cdn.mybank.net; connect-src https://api.mybank.com; frame-src 'self'
实际案例3:只用SSL
一个婚戒论坛管理员希望所有的资源都通过安全的方式进行加载,但是不想真的编写太多代码;重写大量第三方论坛内联脚本和样式的代码超出了他的能力。所以以下的策略将会是非常有用的:
- Content-Security-Policy: default-src https:; script-src https: 'unsafe-inline'; style-src https: 'unsafe-inline'
尽管default-src指定了https,脚本和样式不会自动继承。每个指令将会完全覆盖默认资源类型。
未来
W3C的Web应用安全工作组正在制定内容安全策略规范的细节,1.0版本将要进入最后修订阶段,它和本文描述的内容已经非常接近。而public-webappsec@邮件组正在讨论1.1版本,浏览器厂商也在努力巩固和改进CSP的实现。
CSP 1.1在画板上有一些有趣的地方,值得单独列出来:
通过meta标签添加策略:CSP的首选设置方式是HTTP头,它非常有用,但是通过标记或者脚本设置会更加直接,不过目前还未最终确定。WebKit已经实现了通过meta元素进行权限设置的特性,所以你现在可以在Chrome下尝试如下的设置:在文档头添加<metahttp-equiv="X-WebKit-CSP" content="[POLICY GOES HERE]">。
你甚至可以在运行时通过脚本来添加策略。
DOM API:如果CSP的下一个迭代添加了这个特性,你可以通过Javascript来查询页面当前的安全策略,并根据不同的情况进行调整。例如在eval()是否可用的情况下,你的代码实现可能会有些许不同。这对JS框架的作者来说非常有用;并且API规范目前还非常不确定,你可以在规范草案的脚本接口章节找到最新的迭代版本。
新的指令:许多新指令正在讨论中,包括script-nonce:只有明确指定的脚本元素才能使用内联脚本;plugin-types:这将限制插件的类型;form-action:允许form只能提交到特定的来源。
如果你对这些未来特性的讨论感兴趣,可以阅读邮件列表的归档或者加入邮件列表。