当来自用户控制的来源(如用户名或从网址片段中获取的重定向网址)的数据到达接收器(一个类似于 eval()
的函数或一个属性 setter,如 .innerHTML
)时,就会发生基于 DOM 的跨站脚本攻击 (DOM XSS)。
DOM XSS 是最常见的 Web 安全漏洞之一,开发团队常常会在其应用中意外引入此类漏洞。借助受信任的类型,您可以使用默认安全的危险 Web API 函数编写代码、进行安全审核,并确保应用不存在 DOM XSS 漏洞。对于尚不支持可信类型的浏览器,它们可作为 polyfill 使用。
背景
多年来,DOM XSS 一直是最普遍、最危险的网络安全漏洞之一。
跨站脚本攻击有两种。某些 XSS 漏洞是由服务器端代码不安全地创建构成网站的 HTML 代码而导致的。其他漏洞的根本原因在于客户端,JavaScript 代码会调用使用用户控制的内容的危险函数。
为防止服务器端 XSS,请勿通过串联字符串生成 HTML。请改用安全的上下文自动转义模板库,并搭配使用基于 Nonce 的内容安全政策,以进一步减少 bug。
现在,浏览器还可以通过使用可信类型来帮助防范基于 DOM 的客户端 XSS。
API 简介
可信类型的工作原理是锁定以下有风险的接收器函数。您可能已经知道其中一些,因为浏览器供应商和网站框架已出于安全考虑,建议您不要使用这些功能。
- 脚本操作:
<script src>
以及设置<script>
元素的文本内容。 - 从字符串生成 HTML:
- 执行插件内容:
- 运行时 JavaScript 代码编译:
eval
setTimeout
setInterval
new Function()
使用可信类型时,您需要先处理数据,然后再将其传递给这些接收器函数。仅使用字符串会失败,因为浏览器不知道数据是否可信:
anElement.innerHTML = location.href;
如需表明数据已安全处理,请创建一个特殊对象 - 受信任类型。
anElement.innerHTML = aTrustedHTML;
可信类型可显著缩小应用的 DOM XSS 攻击面。它简化了安全审核,让您可以在运行时在浏览器中强制执行在编译、 lint 或打包代码时进行的基于类型的安全检查。
如何使用可信类型
为内容安全政策违规报告做好准备
您可以部署报告收集器,例如开源 reporting-api-processor 或 go-csp-collector,也可使用某个商业同类产品。您还可以使用 ReportingObserver 在浏览器中添加自定义日志记录和调试违规行为:
const observer = new ReportingObserver((reports, observer) => {
for (const report of reports) {
if (report.type !== 'csp-violation' ||
report.body.effectiveDirective !== 'require-trusted-types-for') {
continue;
}
const violation = report.body;
console.log('Trusted Types Violation:', violation);
// ... (rest of your logging and reporting logic)
}
}, { buffered: true });
observer.observe();
或者,通过添加事件监听器:
document.addEventListener('securitypolicyviolation',
console.error.bind(console));
添加仅报告 CSP 标头
将以下 HTTP 响应标头添加到要迁移到可信类型的文档中:
Content-Security-Policy-Report-Only: require-trusted-types-for 'script'; report-uri //my-csp-endpoint.example
现在,所有违规行为都会报告给 //my-csp-endpoint.example
,但网站会继续正常运行。下一部分将介绍 //my-csp-endpoint.example
的运作方式。
识别 Trusted Types 违规问题
从现在开始,每当可信类型检测到违规行为时,浏览器都会向配置的 report-uri
发送报告。例如,当您的应用将字符串传递给 innerHTML
时,浏览器会发送以下报告:
{
"csp-report": {
"document-uri": "https://my.url.example",
"violated-directive": "require-trusted-types-for",
"disposition": "report",
"blocked-uri": "trusted-types-sink",
"line-number": 39,
"column-number": 12,
"source-file": "https://my.url.example/script.js",
"status-code": 0,
"script-sample": "Element innerHTML <img src=x"
}
}
这表示在 https://my.url.example/script.js
的 39 行中,使用以 <img src=x
开头的字符串调用了 innerHTML
。这些信息应该有助于您缩小代码中可能引入 DOM XSS 且需要更改的部分。
解决违规问题
您可以通过以下几种方式修正 Trusted Type 违规问题。您可以移除违规代码、使用库、创建受信任类型政策,或者作为最后的手段,创建默认政策。
重写违规代码
不合规的代码可能不再需要,或者可以删除导致违规的函数并重写:
el.textContent = ''; const img = document.createElement('img'); img.src = 'xyz.jpg'; el.appendChild(img);
el.innerHTML = '<img src=xyz.jpg>';
使用库
某些库已生成可传递给接收器函数的可信类型。例如,您可以使用 DOMPurify 对 HTML 代码段进行排错,从而移除 XSS 载荷。
import DOMPurify from 'dompurify';
el.innerHTML = DOMPurify.sanitize(html, {RETURN_TRUSTED_TYPE: true});
DOMPurify 支持可信类型,并返回封装在 TrustedHTML
对象中的经过净化的 HTML,以便浏览器不会生成违规问题。
创建“受信任类型”政策
有时,您无法移除导致违规的代码,并且没有库可以对值进行排错并为您创建可信类型。在这些情况下,您可以自行创建可信类型对象。
首先,创建政策。政策是可信类型的工厂,用于对其输入强制执行特定安全规则:
if (window.trustedTypes && trustedTypes.createPolicy) { // Feature testing
const escapeHTMLPolicy = trustedTypes.createPolicy('myEscapePolicy', {
createHTML: string => string.replace(/\</g, '<')
});
}
此代码会创建一个名为 myEscapePolicy
的政策,该政策可以使用其 createHTML()
函数生成 TrustedHTML
对象。定义的规则会对 <
字符进行 HTML 转义,以防止创建新的 HTML 元素。
使用此政策的方式如下:
const escaped = escapeHTMLPolicy.createHTML('<img src=x onerror=alert(1)>');
console.log(escaped instanceof TrustedHTML); // true
el.innerHTML = escaped; // '<img src=x onerror=alert(1)>'
使用默认政策
有时,例如,从 CDN 加载第三方库时,您无法更改违规代码。在这种情况下,请使用默认政策:
if (window.trustedTypes && trustedTypes.createPolicy) { // Feature testing
trustedTypes.createPolicy('default', {
createHTML: (string, sink) => DOMPurify.sanitize(string, {RETURN_TRUSTED_TYPE: true})
});
}
在仅接受可信类型的接收器中使用字符串时,系统会使用名为 default
的政策。
改为强制执行内容安全政策
当您的应用不再产生违规问题后,您可以开始强制执行可信类型:
Content-Security-Policy: require-trusted-types-for 'script'; report-uri //my-csp-endpoint.example
现在,无论您的 Web 应用有多复杂,唯一可能引入 DOM XSS 漏洞的因素就是某个政策中的代码,而且您还可以通过限制政策创建来进一步锁定该代码。