本文为HTML标准解读系列文章,其他文章详见这里。
根据2017年的《开放web应用安全项目》:XSS是最普遍存在的页面漏洞之一,有2/3的web应用存在XSS漏洞。XSS,中文名叫跨站脚本攻击:黑客利用页面的中的漏洞,给页面注入恶意的客户端代码,这些代码被页面认为是受信任的代码,于是可以绕过同源策略的限制并获得相关的用户权限。
常见的避免XSS攻击的手段包括:
-
对所有用户的输入进行过滤,比如对
<、>等敏感符号进行转义。 -
对HTML代码进行“净化”,一个比较有名的库叫DOMPurify,它会把HTML语句中含有XSS风险的的地方过滤掉。如:
DOMPurify.sanitize('<img src=x onerror=alert(1)//>'); // becomes <img src="x"> DOMPurify.sanitize('<svg><g/onload=alert(2)//<p>'); // becomes <svg><g></g></svg> DOMPurify.sanitize('<p>abc<iframe//src=jAva	script:alert(3)>def</p>'); // becomes <p>abc</p> DOMPurify.sanitize('<math><mi//xlink:href="data:x,<script>alert(4)</script>">'); // becomes <math><mi></mi></math> -
使用CSP,内容安全策略。
CSP是本文主要关注的其中一个安全策略,它可以控制页面只执行受信任的脚本、插入受信任的资源。但有时候,我们不得不在页面中引入一些不受控制的第三方页面(如通过iframe),这个时候,我们可以使用沙箱以及权限策略去对第三方页面做一些必要的限制。
CSP的使用
有两种方式可以声明页面的CSP:
-
使用
Content-Security-Policyhttp头:Content-Security-Policy: script-src 'self' -
使用
http-equiv="Content-Security-Policy"的meta标签:<meta http-equiv="Content-Security-Policy" content="script-src 'self'" />
以上的两种声明方式是等价的,表明页面内只允许执行同源的脚本,内联的脚本以及不同源的脚本都会被禁用。大多数使用,两种声明方式可以互相替换。但有的时候,比如使用下面讲到的沙箱机制,就只能使用HTTP的方式。
HTTP声明CSP策略的语句格式是这样的:
Content-Security-Policy: <policy-directive>; <policy-directive>
每个Content-Security-PolicyHTTP头可以有一个或多个策略指令<policy-directive>,每一个策略指令的格式是<directive> <value>。
指令<directive>大约有4种类型,不同类型的指令接受的值<value>可能会有所不同:
-
资源获取指令:这是最常用的指令,声明不同类型的资源的加载规则。如上面的例子中的
script-src表示脚本资源的加载规则;style-src表示样式表资源的加载规则;img-src表示图片资源的加载规则;default-src表示所有类型的资源的默认加载规则......比如,在以下的CSP中,表示页面可以从任何来源加载图片资源,只能从example.com这个站点加载脚本资源,除此以外,其他类型的资源只能来自自己的源。
Content-Security-Policy: default-src 'self'; img-src *; script-src example.com;对于资源获取指令,常用的值
<value>有:self:只允许资源从当前页面的源加载。*:允许资源来自任何来源。nonce-*:对于脚本资源,当nonce属性与该值匹配的时候,就可以被执行。- URL:表示允许资源从该URL加载。
- 无:表示不允许此资源加载。
-
文档指令:控制页面中的特定行为。这一类的指令主要包括
base-uri和sandbox,其中sandbox就是我们下面要讲到的沙箱机制。 -
导航指令:控制页面中的导航行为。这一类指令主要包括
form-action、form-ancestors、navigate-to。 -
报告指令:对页面中违背CSP策略的行为进行上报。
对于所有的指令以及他们的值,可以查看这篇MDN文档。
正如我们前面谈到的,在某些情况下,比如给页面引入第三方广告的时候,我们不得不在页面中插入不受控制的第三方资源,这个时候,我们可以浏览器提供的使用沙箱机制,对第三方页面做一些必要的限制,比如禁用某些功能。
沙箱
你可以使用csp的sandbox指令表示整个页面是一个沙箱,这个时候,页面中的下载、表单提交、脚本执行等多种能力都会被禁用,并且这些限制会作用在页面中所有的“子页面”,如iframe:
Content-Security-Policy: sandbox;
如果你想要启动某个功能,比如下载,你需要显式声明这个功能对应的关键词:
Content-Security-Policy: sandbox allow-downloads;
更多的情况下,我们可能只会单独对一个iframe启用沙箱机制。iframe提供sandbox属性启动相同的沙箱机制,当声明这个属性的时候,iframe会被当作一个独立的源,所以自动触发同源策略的所有约束,并且禁用许多有安全风险的功能,包括不允许在主文档中进行导航。
<iframe src="https://example.com" sandbox>
对于iframe的sandbox属性的其他使用,包括各种功能对应的关键词,我在iframe相关的安全特性已经列出了完整的列表。
不过,沙箱机制也是有它的缺陷的:
- 从上面的内容我们知道,沙箱的机制是先禁用所有的功能,再逐个启用,而对于那些原本就是禁用的功能,没有启用的机制;
- 将来要对沙箱机制进行扩展的时候,面临着很大的兼容性风险;
- 沙箱也无法像其他的CSP策略一样,对特定URL的页面启动沙箱。
而「权限策略」,就是针对沙箱机制的这种不足的补充。
权限策略
相比于沙箱机制,权限策略与CSP的资源获取指令的作用方式更加相似,它可以针对特定的URL做特定的配置。使用Permission-PolicyHTTP头可以声明页面使用的权限策略,比如下面的例子中,表示只在同源的、或者example.com 的页面启用获取地理位置的功能:
Permissions-Policy: geolocation=(self "https://example.com")
跟沙箱一样,权限策略也可以单独作用在iframe上,具体使用iframe的allow属性。比如下面的例子中,默认情况下,不同源的iframe的地理位置功能是禁用的,通过声明allow属性可以启用这个功能:
<iframe src="https://other-origin.example" allow="geolocation">
权限策略比沙箱机制更加灵活,也因此它所关联的浏览器功能比沙箱机制多得多。你可以使用document.featurePolicy.features()查看浏览器支持的权限策略的列表(featurePolicy是权限策略的曾用名):
console.log(document.featurePolicy.features()) // (51)['geolocation','gamepad','ch-ect','midi',...]
对于这个列表中每一项的具体意义,你可以参考这个github仓库。
值得注意的是,根据权限策略的规范,权限策略并不是沙箱机制的替代,而是一种补充,所以在设计的时候这两种安全机制并不会有功能上的重叠,他们被鼓励放在一起使用。
对于权限策略更多的使用细节,你可以查看谷歌的这篇文章。