使用可信类型防范基于 DOM 的跨站脚本攻击漏洞

Krzysztof Kotowicz
Krzysztof Kotowicz

浏览器支持

  • 83
  • 83
  • x
  • x

来源

当来自用户控制的来源(如用户名或从网址片段中获取的重定向网址)的数据到达接收器(一个类似于 eval() 的函数或一个属性 setter,如 .innerHTML)时,就会发生基于 DOM 的跨站脚本攻击 (DOM XSS)。

DOM XSS 是最常见的 Web 安全漏洞之一,开发团队经常会在其应用中意外引入该漏洞。可信类型为您提供了编写代码、进行安全审核以及确保应用免受 DOM XSS 漏洞攻击的工具,它会默认确保危险的 Web API 函数安全无虞。对于尚不支持可信类型的浏览器,它们可作为 polyfill 使用。

背景

多年来,DOM XSS 一直是最普遍、最危险的网络安全漏洞之一。

跨站脚本攻击有两种。一些 XSS 漏洞是由以不安全的方式创建组成网站的 HTML 代码的服务器端代码引起的。而另一些则是客户端上的根本原因,在这种情况下,JavaScript 代码会使用由用户控制的内容调用危险函数。

防止服务器端 XSS,请勿通过串联字符串来生成 HTML。请改用安全的上下文自动转义模板库以及基于 Nonce 的内容安全政策,以减少额外的 bug。

现在,浏览器还可以使用可信类型来阻止基于 DOM 的客户端 XSS。

API 简介

可信类型的工作原理是锁定以下有风险的接收器函数。您可能已经认识到其中一些功能,因为浏览器供应商和 Web 框架出于安全考虑已经禁止您使用这些功能。

可信类型要求您先处理数据,然后再将其传递给这些接收器函数。仅使用字符串会失败,因为浏览器不知道数据是否可信:

错误做法
anElement.innerHTML  = location.href;
启用可信类型后,浏览器会抛出 TypeError 并阻止使用带有字符串的 DOM XSS 接收器。

为了表示数据已得到安全处理,请创建一个特殊的对象,即信任类型。

正确做法
anElement.innerHTML = aTrustedHTML;
  
启用可信类型后,对于需要 HTML 代码段的接收器,浏览器会接受 TrustedHTML 对象。此外,还有适用于其他敏感接收器的 TrustedScriptTrustedScriptURL 对象。

可信类型可显著减小应用的 DOM XSS 攻击面。它简化了安全审核,并且可让您在浏览器中对代码进行编译、执行 lint 请求或捆绑代码时强制执行基于类型的安全检查。

如何使用可信类型

为收到内容安全政策违规行为举报做好准备

您可以部署报告收集器,例如开源 reporting-api-processorgo-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 的工作原理。

识别可信类型违规行为

从现在开始,每当可信类型检测到违规行为时,浏览器都会向已配置的 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"
}
}

这表示在第 39 行的 https://my.url.example/script.js 中,使用以 <img src=x 开头的字符串调用了 innerHTML。这些信息可帮助您缩小可能引入 DOM XSS 且需要更改的代码部分。

修正违规问题

您可以通过多种方法解决可信类型违规行为。您可以移除违规代码使用库创建可信类型政策,或者,最后的解决方法是创建默认政策

重写违规代码

可能不再需要不符合规定的代码,或者可以在不使用导致违规行为的函数的情况下重写这些代码:

正确做法
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, '&lt;')
  });
}

此代码会创建一个名为 myEscapePolicy 的政策,该政策可以使用其 createHTML() 函数生成 TrustedHTML 对象。定义的规则会对 < 字符进行 HTML 转义,以防止创建新的 HTML 元素。

请使用如下政策:

const escaped = escapeHTMLPolicy.createHTML('<img src=x onerror=alert(1)>');
console.log(escaped instanceof TrustedHTML);  // true
el.innerHTML = escaped;  // '&lt;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 漏洞的都是某个政策中的代码,您可以通过限制政策创建来进一步锁定该漏洞。

深入阅读