使用 Trusted Types 防范基于 DOM 的 XSS 攻击

Web 功能: 可信类型

可信类型是一项安全功能,它要求将传入“危险”浏览器 API(称为接收器)的数据首先转换为特定类型(例如 TrustedHTML),从而帮助防范基于 DOM 的跨站脚本攻击 (DOM XSS)。

默认情况下,许多 DOM API(例如 innerHTML 或 document.write)都接受原始字符串。如果字符串包含来自不可信来源的恶意脚本,则该脚本可能会被执行。可信类型会改变这种行为:通过 CSP 启用后,这些接收器将仅接受由授权政策创建的 TrustedHTML 对象。

关键实现细节

如需使用 Trusted Types,请遵循以下四个主要步骤:

  1. 定义安全政策,指定如何清理或转换字符串。
  2. 使用该政策从原始字符串创建可信类型对象。
  3. 写入 DOM 接收器时,请使用 Trusted Type 对象。
  4. 使用内容安全政策 (CSP) 强制执行可信类型。

1. 创建政策

政策是一组用于清理内容的规则。您可以使用 trustedTypes.createPolicy() 创建它,通常会委托给净化器库(例如 DOMPurify)或应用的 HTML 转义规则:

import DOMPurify from 'dompurify';

const myPolicy = trustedTypes.createPolicy('sanitize-html', {
  createHTML: (input) =>
    // Return a sanitized string; createPolicy() wraps it in a TrustedHTML object.
    // Example using DOMPurify:
    DOMPurify.sanitize(input, { RETURN_TRUSTED_TYPE: false }),
});

// Example using HTML escaping (for environments without third-party sanitizers):
const escapePolicy = trustedTypes.createPolicy('escape-html', {
  createHTML: (input) =>
    String(input)
      .replaceAll('&', '&')
      .replaceAll('<', '&lt;')
      .replaceAll('>', '&gt;')
      .replaceAll('"', '&quot;')
      .replaceAll("'", '&#39;'),
});

2. 使用政策

您无需将字符串直接传递给接收器,而是调用政策的创建方法。

const untrustedInput = '<p>Hello, <strong>world</strong>!</p><img src="x" onerror="alert(1)">';

// In an enforced environment, this would throw a TypeError:
// element.innerHTML = untrustedInput;

// This returns a TrustedHTML object with unsafe elements and attributes removed:
const cleanHTML = myPolicy.createHTML(untrustedInput);
// Result: "<p>Hello, <strong>world</strong>!</p><img src=\"x\">"

3. 将接收器转换为使用可信类型

更新您向接收器写入内容的每个实例,以便先将其转换为 Trusted Type 对象。为了帮助找到需要更新代码的位置,您可以定义一个临时 default 政策,以便在接收器被写入字符串(而非 TrustedType)时进行记录。

trustedTypes.createPolicy("default", {
  createHTML(value) {
    console.warn("String passed instead of Trusted Type.");
    return DOMPurify.sanitize(value, { RETURN_TRUSTED_TYPE: false });
  },
});

支持可信类型的常见接收器包括:

  • HTML: element.innerHTML、element.outerHTML、document.write()、element.setHTMLUnsafe()、Document.parseHTMLUnsafe()
  • 脚本: <script> 文本内容,eval()
  • ScriptURL: <script src>、Worker()、SharedWorker()
// Assign the TrustedHTML object.
element.innerHTML = cleanHTML;

请注意,“安全”HTML 清理方法(element.setHTML() 和 Document.parseHTML(),在清理不受信任的 HTML 中介绍)不需要可信类型政策,因为它们始终会以原生方式清理潜在的 XSS 攻击。

4. 通过 CSP 强制执行 Trusted Types

应逐步部署可信类型,以免破坏应用。虽然全面强制执行可提供最强大的保护,但如果并非所有接收器写入都已迁移,则存在网站损坏的风险。

前提条件

  • 框架协作:如果应用的框架(或任何写入 DOM 接收器的第三方库)不生成 TrustedHTML 或 TrustedScript 值,则您无法在不破坏该代码的情况下强制执行可信类型。在开始发布之前,请先审核框架支持。
  • 接收器重构:确保您已识别并重构危险接收器,以便使用可信类型政策。

发布策略

  1. 仅报告:首先,使用 Content-Security-Policy-Report-Only 标头进行发布。这会将违规行为记录到报告端点,而不会实际阻止写入。

    Content-Security-Policy-Report-Only: require-trusted-types-for 'script';
    
  2. 强制执行:集成政策且违规报告降至零后,即可进入全面强制执行阶段。

    Content-Security-Policy: require-trusted-types-for 'script';
    

您还可以限制允许创建哪些政策(列出您的政策名称,例如 sanitize-html 或 escape-html;如果您的政策委托给 DOMPurify.sanitize(),则添加 dompurify,这会在内部创建 dompurify 政策;如果您在迁移期间注册了默认政策,则添加 default):

Content-Security-Policy: require-trusted-types-for 'script'; trusted-types sanitize-html escape-html dompurify;

实施此 CSP 后,任何不使用可信类型的接收器写入操作都会抛出错误。此外,如果尝试创建名称未在 trusted-types 中列出的可信类型政策,系统会抛出错误。

您也可以在文档的 <head> 中将此 CSP 设置为 <meta> 标记,但这样做的安全性可能较低。

<!-- First child of `<head>`, as earlier tags will not enforce this CSP. -->
<meta
  http-equiv="Content-Security-Policy"
  content="require-trusted-types-for 'script'; trusted-types sanitize-html escape-html dompurify;"
/>

后备策略

对于尚不支持可信类型的浏览器,您可以使用最精简的“tinyfill”来确保代码不会中断,同时仍提供清理功能。

if (!window.trustedTypes) {
  window.trustedTypes = {
    createPolicy: (name, rules) => rules,
  };
}

这样,您现在就可以使用可信类型模式编写代码。支持该 API 的浏览器会强制执行这些类型,而旧版浏览器仍会通过模拟政策执行您的清理逻辑。

如果浏览器不支持可信类型,则只会将 TrustedHTML 对象视为字符串(如果使用 tinyfill),或者不会在接收器级别进行强制执行检查。