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

Krzysztof Kotowicz
Krzysztof Kotowicz

浏览器支持

  • Chrome:83。
  • Edge:83.
  • Firefox:不受支持。
  • Safari:不受支持。

来源

当来自用户控制的来源(例如用户名或从网址 fragment 中获取的重定向网址)的数据到达接收器(即可以执行任意 JavaScript 代码的 eval() 等函数或 .innerHTML 等属性设置器)时,就会发生基于 DOM 的跨站脚本攻击 (DOM XSS)。

DOM XSS 是最常见的 Web 安全漏洞之一,开发团队常常会在其应用中意外引入此类漏洞。受信任的类型可让您通过默认使危险的 Web API 函数安全,从而获得编写、安全审核和确保应用不存在 DOM XSS 漏洞的工具。可信类型可作为polyfill 提供给尚不支持它们的浏览器。

背景

多年来,DOM XSS 一直是最常见且最危险的 Web 安全漏洞之一。

跨网站脚本有两种类型。某些 XSS 漏洞是由服务器端代码不安全地创建构成网站的 HTML 代码而导致的。其他漏洞的根本原因在于客户端,JavaScript 代码会调用使用用户控制的内容的危险函数。

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

现在,浏览器还可以通过使用可信类型来帮助防范基于 DOM 的客户端 XSS。

API 简介

可信类型的运作方式是锁定以下风险较高的接收器函数。您可能已经知道其中一些,因为浏览器供应商和网站框架已出于安全考虑,建议您不要使用这些功能。

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

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

如需表明数据已安全处理,请创建一个特殊对象 - 受信任类型。

正确做法
anElement.innerHTML = aTrustedHTML;
  
启用可信类型后,浏览器会接受 TrustedHTML 对象,以便接收预期为 HTML 代码段的接收器。此外,还有 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 的运作方式。

识别 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,以便浏览器不会生成违规问题。

创建“受信任类型”政策

有时,您无法移除导致违规的代码,并且没有库可以对值进行排错并为您创建可信类型。在这些情况下,您可以自行创建可信类型对象。

首先,创建政策。政策是 Trusted Types 的工厂,用于对其输入强制执行特定安全规则:

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 漏洞的因素就是某个政策中的代码,您可以通过限制政策创建来进一步锁定该代码。

深入阅读