内容安全策略(CSP)中的 Trusted Types 机制:在 DOM 级别强制执行类型安全以根除 XSS 注入

尊敬的各位专家、开发者同仁们:

大家好!

今天,我们将深入探讨一个在现代Web安全领域日益重要的机制——内容安全策略(CSP)中的 Trusted Types。Web应用的安全挑战从未停止,其中跨站脚本(XSS)攻击一直是前端安全领域的“万恶之源”,它像一个狡猾的幽灵,潜伏在Web应用的各个角落,随时准备将不可信的恶意代码注入到用户的浏览器中。

传统的安全措施,包括我们熟知的CSP,在很大程度上提升了Web应用的防御能力。CSP通过限制资源的加载来源,有效抵御了许多类型的XSS攻击。然而,面对日益复杂和精妙的DOM-based XSS攻击,仅仅依靠源限制是远远不够的。Trusted Types 正是为了弥补这一空白而生,它在DOM层面强制执行类型安全,从根本上杜绝了不可信字符串被注入到敏感DOM操作中的风险,从而有效地根除了一大类XSS注入。

本次讲座,我将以编程专家的视角,详细剖析 Trusted Types 的工作原理、配置方法、实际应用及最佳实践,并辅以大量的代码示例,旨在帮助大家深刻理解并掌握这一强大的安全工具。

XSS攻击的持久威胁与DOM层面的脆弱性

在深入了解 Trusted Types 之前,我们必须首先回顾XSS攻击的本质及其在DOM层面的表现。XSS攻击,全称Cross-Site Scripting,是指攻击者将恶意脚本注入到受信任的网站中,当用户访问该网站时,恶意脚本会在用户的浏览器上执行。根据攻击方式和数据存储位置,XSS通常分为三类:

  1. 反射型XSS (Reflected XSS):恶意脚本通过URL参数等形式传递给服务器,服务器未经净化直接将脚本“反射”回浏览器执行。
  2. 存储型XSS (Stored XSS):恶意脚本被永久存储在目标服务器(如数据库)中,当用户访问包含该脚本的页面时,脚本会被加载并执行。
  3. DOM-based XSS (DOM-based XSS):这是 Trusted Types 主要针对的类型。恶意脚本不经过服务器,而是在浏览器端,通过修改页面DOM环境来执行。其核心在于,客户端脚本从URL、localStorage、cookie等不可信源读取数据,然后将这些数据动态地写入到DOM中敏感的“接收器”(sink),从而导致恶意代码的执行。

DOM-based XSS 的危险性在于,即使后端严格过滤了输入,即使CSP限制了脚本的来源,如果前端代码自身在处理用户输入时存在缺陷,攻击者仍然可以通过操纵浏览器端的数据流来发动攻击。

危险的DOM接收器(Sinks)

Web浏览器提供了许多API,允许JavaScript动态地操作页面的结构和内容。然而,其中一些API如果被赋予了未经严格审查的字符串数据,就会成为XSS攻击的温床。我们称这些API为“危险的DOM接收器”。理解这些接收器是理解 Trusted Types 机制为何如此重要的关键。

以下是一些最常见的危险DOM接收器:

  • HTML内容注入

    • Element.innerHTML:最常见的XSS向量之一。将一个包含恶意HTML或脚本的字符串赋给它,浏览器就会解析并执行。
      // 恶意示例:
      const userInput = "<img src='x' onerror='alert("XSS via innerHTML!")'>";
      document.getElementById('content').innerHTML = userInput; // 危险!
    • Element.outerHTML:类似 innerHTML,但会替换整个元素。
    • Element.insertAdjacentHTML(position, text):在指定位置插入HTML字符串。
    • document.write() / document.writeln():直接将字符串写入文档流。
  • 脚本执行

    • script.src:设置 script 元素的 src 属性,加载并执行外部脚本。
      // 恶意示例:
      const evilScriptUrl = "https://evil.com/malicious.js";
      const scriptElem = document.createElement('script');
      scriptElem.src = evilScriptUrl; // 危险!
      document.body.appendChild(scriptElem);
    • eval():直接执行字符串作为JavaScript代码。
    • setTimeout() / setInterval():当第一个参数是字符串时,会将其作为JavaScript代码执行。
      // 恶意示例:
      const maliciousCode = "alert('XSS via setTimeout!')";
      setTimeout(maliciousCode, 100); // 危险!
    • new Function():从字符串创建一个新的函数。
    • Event Handlers (e.g., onclick, onerror):通过 setAttribute 或直接赋值设置事件处理函数时,如果值是字符串,也会被执行。
      // 恶意示例:
      const img = document.createElement('img');
      img.setAttribute('src', 'x');
      img.setAttribute('onerror', 'alert("XSS via attribute event handler!")'); // 危险!
      document.body.appendChild(img);
  • URL加载与跳转

    • window.location.href / window.location.assign() / window.location.replace():跳转到新的URL。如果URL是 javascript: 伪协议,则会执行JS代码。
      // 恶意示例:
      const maliciousUrl = "javascript:alert('XSS via location.href!')";
      window.location.href = maliciousUrl; // 危险!
    • Element.setAttribute(attributeName, value):当 attributeNamehref (for <a>, <link>), src (for <img>, <iframe>), formaction (for <form>), baseURI 等时,如果 valuejavascript: 伪协议或指向恶意资源,则构成风险。
      // 恶意示例:
      const link = document.createElement('a');
      link.setAttribute('href', "javascript:alert('XSS via href attribute!')"); // 危险!
      document.body.appendChild(link);
    • iframe.src:加载外部内容到iframe中。
    • object.data:加载外部内容到object元素中。
    • embed.src:加载外部内容到embed元素中。
  • 样式表注入

    • CSSStyleSheet.insertRule():插入CSS规则。如果规则包含 url()expression() (IE旧版本)等,可能导致XSS或数据泄露。
    • element.style.cssText:直接设置元素的内联样式。

以上这些危险的DOM接收器,是XSS攻击者利用的主要途径。传统的防御手段,如输入净化(sanitization)和编码(encoding),虽然重要,但往往难以做到滴水不漏,一旦疏忽,便可能导致安全漏洞。而CSP,尽管强大,也存在其局限性,特别是在防御DOM-based XSS方面。

内容安全策略 (CSP) 的作用与局限性

内容安全策略(CSP)是一种HTTP响应头,旨在通过指定浏览器可以加载哪些资源(如脚本、样式、图片、字体等)的来源,从而减少XSS和其他代码注入攻击的风险。

CSP 的核心能力

CSP通过一系列指令来工作,例如:

  • script-src:限制JavaScript的来源。
  • style-src:限制CSS的来源。
  • img-src:限制图片的来源。
  • default-src:为所有未明确指定的资源类型设置默认策略。
  • object-src:限制插件(如Flash)的来源。
  • base-uri:限制文档中 <base> 元素可以引用的URL。
  • require-trusted-types-for:这是我们今天的主角,它启用 Trusted Types

CSP的工作原理是,浏览器在解析页面时,会检查所有资源的加载请求是否符合CSP策略。如果不符合,浏览器将阻止该资源的加载并可能报告违规。

CSP 示例:

Content-Security-Policy: default-src 'self'; script-src 'self' https://trusted.cdn.com; object-src 'none'; base-uri 'self'

这个CSP策略表示:

  • 所有未明确指定的资源(default-src)只能从当前源('self')加载。
  • 脚本(script-src)可以从当前源或 https://trusted.cdn.com 加载。
  • 插件(object-src)完全不允许加载('none')。
  • <base> 元素只能引用当前源。

通过这样的策略,CSP可以有效地阻止攻击者注入外部恶意脚本,或者通过 <base> 标签劫持资源路径。

CSP 在防御 DOM-based XSS 上的局限性

尽管CSP非常强大,但在防御DOM-based XSS方面,它存在固有的局限性。这些局限性正是 Trusted Types 诞生的根本原因:

  1. 无法区分同源脚本内的恶意数据流:CSP关注的是脚本的 来源。如果一个脚本是从允许的源加载的(例如,'self'),那么CSP就会允许它执行。但如果这个 合法加载的脚本 内部存在DOM-based XSS漏洞,它从用户输入中获取一个恶意字符串,并将其赋给 innerHTML,CSP无法阻止。因为CSP只知道这个脚本是合法的,它无法深入到脚本的执行逻辑中去判断数据的安全性。

    <!-- CSP: script-src 'self' -->
    <!-- 允许加载 index.js -->
    <script src="index.js"></script>
    <div id="user-content"></div>
    
    <!-- index.js -->
    <script>
        // 假设 `data` 是从 URL 参数或 API 响应中获取的不可信字符串
        const data = new URLSearchParams(window.location.search).get('input');
        // CSP 允许 index.js 运行,但无法阻止这段代码的 XSS 行为
        document.getElementById('user-content').innerHTML = data; // XSS 漏洞!
    </script>

    在这个例子中,index.js 是同源脚本,CSP会允许它执行。但如果 data 包含 <img src='x' onerror='alert(1)'>,XSS攻击仍然会发生。

  2. 'unsafe-inline' 的无奈:许多遗留应用或复杂应用由于使用了大量的内联脚本或内联事件处理器(如 onclick="doSomething()"),不得不使用 script-src 'unsafe-inline'。一旦 unsafe-inline 被启用,CSP在很大程度上就失去了对内联脚本的保护能力,攻击者可以轻易地注入内联脚本。

  3. eval()setTimeout(string, ...) 等的限制不够精细:虽然CSP可以通过 script-src 'unsafe-eval' 来禁止 eval() 等,但如果应用确实需要动态执行代码(例如模板引擎),则不得不启用它,这又引入了风险。CSP无法区分 eval('1+1')eval('alert(1)')

  4. CSP 报告机制的滞后性:CSP的违规报告通常是事后触发的,虽然有助于发现漏洞,但无法在攻击发生前阻止。

正是因为这些局限性,Web平台需要一种更细粒度、更接近DOM操作层面的安全机制,来补充CSP的不足。Trusted Types 应运而生,它提供了一种在运行时强制执行DOM类型安全的强大方式。

引入 Trusted Types:DOM层面的类型安全防护

Trusted Types 机制的核心思想是:阻止任何“普通字符串”被直接赋值给那些可能导致XSS的危险DOM接收器。 相反,这些接收器现在只接受特殊的“可信类型”(Trusted Type)对象。这些可信类型对象不是凭空产生的,它们必须通过开发者明确定义的“策略”(Policy)来创建。

这意味着,当 Trusted Types 被启用后,你不能再简单地写 element.innerHTML = someString;。如果 someString 是一个普通的JavaScript字符串,浏览器会抛出一个错误,阻止赋值操作。你必须先通过一个你信任的函数(即一个 Trusted Type Policy)将 someString 转换成一个 TrustedHTML 对象,然后才能赋值。

Trusted Types 的核心目标

  • 根除DOM-based XSS:通过强制要求所有危险DOM操作必须使用经过验证的、类型化的数据,从源头上阻止恶意字符串的注入。
  • 强化安全审查:开发者在代码中处理任何可能进入危险接收器的数据时,都必须显式地调用 Trusted Type Policy。这使得代码审查者更容易识别潜在的风险点,因为他们可以专注于审查这些策略的实现。
  • 消除 unsafe-inline 的需求:在许多情况下,通过使用 Trusted Types,可以避免在 script-src 指令中使用 'unsafe-inline',从而显著增强CSP的整体安全性。

Trusted Types 如何工作?

  1. 启用强制执行:通过CSP头部指令 require-trusted-types-for 'script' 来启用 Trusted Types 的强制执行。
  2. 定义策略:开发者需要定义JavaScript函数(称为 Trusted Type Policy),这些函数负责接收原始字符串数据,对其进行净化、验证或转义,然后返回一个 Trusted Type 对象(如 TrustedHTML)。
  3. 创建可信对象:当需要将数据插入到危险DOM接收器时,不再直接使用原始字符串,而是调用先前定义的策略函数来创建对应的 Trusted Type 对象。
  4. 浏览器验证:浏览器在执行危险DOM操作时,会检查传入的值是否是一个合法的 Trusted Type 对象。如果是,则允许操作;如果是一个普通字符串,则抛出 TypeError 错误。

这种机制迫使开发者在处理任何可能导致XSS的数据流时,都必须经过一个明确的、可审计的“安全关卡”。

启用 Trusted Types

要启用 Trusted Types,你需要通过HTTP响应头来配置你的内容安全策略(CSP)。

CSP Header 配置

Trusted Types 主要通过CSP的两个指令来控制:require-trusted-types-fortrusted-types

  1. require-trusted-types-for 指令
    该指令告诉浏览器,对于特定类型的DOM接收器,必须强制执行 Trusted Types。目前,它支持 'script''object' 作为参数。

    • require-trusted-types-for 'script':这是最常用的,它会为所有可能执行脚本的DOM接收器(如 innerHTMLscript.srcsetTimeout(string, ...) 等)启用 Trusted Types 强制执行。
    • require-trusted-types-for 'object':当 object 元素用于加载内容时,启用 Trusted Types

    示例:

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

    这个策略启用了对所有脚本相关DOM接收器的 Trusted Types 强制执行。

  2. trusted-types 指令
    该指令用于声明你的应用允许哪些 Trusted Type Policy 的名称。每个策略都有一个唯一的名称。只有在这里列出的策略名称,浏览器才允许它们被 trustedTypes.createPolicy() 调用创建。特殊值 * 允许所有策略名称。

    示例:

    Content-Security-Policy: require-trusted-types-for 'script'; trusted-types my-sanitizer my-url-policy; default-src 'self'

    这个策略不仅启用了 Trusted Types 强制执行,还明确指出只有名为 my-sanitizermy-url-policy 的策略才能被创建。这增加了额外的安全层,防止恶意代码创建自己的不可信策略。

    允许所有策略名称(不推荐用于生产环境):

    Content-Security-Policy: require-trusted-types-for 'script'; trusted-types *; default-src 'self'

    使用 * 应该非常谨慎,通常只在开发或迁移阶段使用,因为它降低了对策略名称的限制。

报告模式(Report-Only Mode)

与所有CSP指令一样,Trusted Types 也可以在报告模式下运行。这对于在不中断现有应用功能的情况下测试和识别违规非常有用。

Content-Security-Policy-Report-Only: require-trusted-types-for 'script'; trusted-types my-sanitizer my-url-policy; report-uri /csp-report-endpoint

在这个模式下,浏览器会继续允许不符合 Trusted Types 要求的操作,但会将违规报告发送到 /csp-report-endpoint。这允许开发者收集违规信息,逐步修复代码,而不会立即破坏生产应用。

页面内的配置(不推荐)

虽然理论上可以通过 <meta> 标签配置CSP,但对于 require-trusted-types-for 这样的指令,不建议使用 <meta> 标签。CSP头部是首选且更安全的配置方式,因为它在任何内容被解析之前就已生效。

Trusted Type Policies:创建安全数据的工厂

Trusted Type PoliciesTrusted Types 机制的核心。它们是应用程序中经过严格审查和信任的代码,负责将原始的、可能不安全的字符串转换为安全的 Trusted Type 对象。

全局 trustedTypes 对象

浏览器提供了一个全局的 trustedTypes 对象(在 window.trustedTypes 下),用于创建和管理策略。

if (window.trustedTypes && trustedTypes.createPolicy) {
    // Trusted Types API可用
}

创建策略 trustedTypes.createPolicy()

trustedTypes.createPolicy(policyName, policyConfig) 方法用于创建一个新的 Trusted Type Policy

  • policyName (字符串):策略的唯一名称。这个名称必须在CSP的 trusted-types 指令中被允许。
  • policyConfig (对象):一个包含回调函数的对象,这些函数定义了如何创建不同类型的 Trusted Type 对象。

policyConfig 可以包含以下回调函数:

  • createHTML(input):接收一个字符串,返回一个 TrustedHTML 对象。用于 innerHTML 等HTML接收器。
  • createScript(input):接收一个字符串,返回一个 TrustedScript 对象。用于 eval()setTimeout(string, ...) 等脚本执行接收器。
  • createScriptURL(input):接收一个字符串,返回一个 TrustedScriptURL 对象。用于 script.srcimport() 动态导入等脚本URL接收器。
  • createURL(input):接收一个字符串,返回一个 TrustedURL 对象。用于 a.hrefiframe.srcwindow.location.href 等通用URL接收器。
  • createStyle(input):接收一个字符串,返回一个 TrustedStyle 对象。用于 CSSStyleSheet.insertRule() 等样式相关接收器。

策略设计原则

一个安全有效的策略应该遵循以下原则:

  1. 明确的输入验证和净化:策略函数必须严格验证和净化输入。对于HTML内容,这意味着删除所有不安全的标签和属性;对于URL,这意味着验证协议、域名和路径。
  2. 上下文相关的转义:根据数据的使用上下文(HTML、JavaScript、URL),进行适当的转义。例如,将用户输入插入到HTML属性中需要属性转义,插入到JavaScript字符串中需要JavaScript字符串转义。
  3. 最小权限原则:策略应该只允许最少的功能集,以满足应用需求,避免过于宽松的规则。
  4. 使用已验证的安全库:在策略内部,可以并且应该利用已有的、经过安全审计的库,如 DOMPurify 用于HTML净化。
  5. 避免直接返回输入:除非你确定输入是完全静态且安全的,否则不应直接 return input;。这是最常见的导致 Trusted Types 绕过的陷阱。

代码示例:创建不同类型的策略

1. TrustedHTML 策略示例 (结合 DOMPurify)

这个策略用于处理可能包含HTML的字符串,并将其净化为安全的可信HTML。

// CSP 配置示例:
// Content-Security-Policy: require-trusted-types-for 'script'; trusted-types myHTMLPolicy; default-src 'self'

// 假设我们已经引入了 DOMPurify 库
// <script src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/2.3.6/purify.min.js"></script>

let myHTMLPolicy;

if (window.trustedTypes && trustedTypes.createPolicy) {
    myHTMLPolicy = trustedTypes.createPolicy('myHTMLPolicy', {
        createHTML: (input) => {
            // 在这里执行HTML净化逻辑
            // 使用 DOMPurify 库进行净化
            const cleanHTML = DOMPurify.sanitize(input, {
                USE_PROFILES: { html: true } // 或者其他适合您需求的配置
            });
            console.log('Processed HTML:', cleanHTML);
            return cleanHTML; // DOMPurify 返回的是字符串,Trusted Types 会自动包装成 TrustedHTML
        },
        createScript: (input) => {
            // 如果您的应用需要动态创建脚本,但这个策略主要处理HTML,
            // 那么此处可以抛出错误或返回空字符串,或者指向一个专门的脚本策略。
            // 示例:阻止任意脚本
            throw new Error('myHTMLPolicy does not support createScript');
        },
        createScriptURL: (input) => {
             throw new Error('myHTMLPolicy does not support createScriptURL');
        }
        // ... 其他类型也应根据需要定义或拒绝
    });
} else {
    // Trusted Types 不可用时的降级处理
    console.warn('Trusted Types not supported or enabled. Proceeding without it.');
    myHTMLPolicy = {
        createHTML: (input) => input, // 危险!在生产环境中不要这样做
        createScript: (input) => input,
        createScriptURL: (input) => input,
        createURL: (input) => input,
        createStyle: (input) => input
    };
}

// 如何使用:
const userComment = "<p>Hello <b>World</b>!</p><img src='x' onerror='alert("XSS!")'>";
const targetDiv = document.getElementById('comment-section');

try {
    if (myHTMLPolicy) {
        // 使用策略创建 TrustedHTML 对象
        targetDiv.innerHTML = myHTMLPolicy.createHTML(userComment);
    } else {
        // 降级处理,直接赋值,存在XSS风险
        targetDiv.innerHTML = userComment;
    }
} catch (e) {
    console.error('Failed to set innerHTML with Trusted Types:', e);
}

// 假设 DOMPurify 移除了 img 标签,alert 不会执行。
// 如果直接赋值 userComment,则 alert 会执行。

注意DOMPurify.sanitize() 返回的是一个普通的字符串。Trusted TypescreateHTML 等回调函数返回字符串时,会自动将其包装成对应的 TrustedType 对象。这是设计好的行为,因为策略函数的职责是确保字符串内容本身是安全的。

2. TrustedScriptURL 策略示例

这个策略用于验证和生成可信的脚本URL。通常,我们会限制脚本只能从特定白名单域加载,或者只加载静态、已知的脚本文件。

// CSP 配置示例:
// Content-Security-Policy: require-trusted-types-for 'script'; trusted-types myScriptURLPolicy; default-src 'self'

let myScriptURLPolicy;

if (window.trustedTypes && trustedTypes.createPolicy) {
    myScriptURLPolicy = trustedTypes.createPolicy('myScriptURLPolicy', {
        createScriptURL: (url) => {
            // 简单的白名单验证
            const trustedDomains = ['https://cdn.example.com', 'https://api.example.com'];
            const urlObj = new URL(url, window.location.origin); // 确保URL是绝对的

            if (trustedDomains.some(domain => urlObj.origin === new URL(domain).origin)) {
                return url; // 允许白名单域的URL
            }
            // 也可以允许同源的相对路径脚本
            if (urlObj.origin === window.location.origin && !url.startsWith('javascript:')) {
                return url;
            }

            console.error('Untrusted script URL blocked:', url);
            throw new Error('Untrusted script URL');
        },
        createScript: (input) => {
            throw new Error('myScriptURLPolicy does not support createScript');
        },
        createHTML: (input) => {
            throw new Error('myScriptURLPolicy does not support createHTML');
        }
    });
} else {
    myScriptURLPolicy = {
        createScriptURL: (url) => url,
        createScript: (input) => input,
        createHTML: (input) => input,
        createURL: (input) => input,
        createStyle: (input) => input
    };
}

// 如何使用:
const scriptElem = document.createElement('script');
const userProvidedScript = "https://cdn.example.com/safe-script.js";
// const maliciousScript = "https://evil.com/malicious.js";

try {
    if (myScriptURLPolicy) {
        scriptElem.src = myScriptURLPolicy.createScriptURL(userProvidedScript);
        document.body.appendChild(scriptElem);
    } else {
        scriptElem.src = userProvidedScript;
        document.body.appendChild(scriptElem);
    }
} catch (e) {
    console.error('Failed to load script with Trusted Types:', e);
}

3. TrustedScript 策略示例

这个策略用于处理需要作为JavaScript代码执行的字符串,如 eval()setTimeout() 的第一个参数。通常,我们应该尽量避免这种动态执行代码的需求。如果必须,应极其谨慎。

// CSP 配置示例:
// Content-Security-Policy: require-trusted-types-for 'script'; trusted-types myScriptPolicy; default-src 'self'

let myScriptPolicy;

if (window.trustedTypes && trustedTypes.createPolicy) {
    myScriptPolicy = trustedTypes.createPolicy('myScriptPolicy', {
        createScript: (code) => {
            // 这个示例策略非常严格:只允许执行硬编码的脚本片段。
            // 在实际应用中,您可能需要更复杂的逻辑,例如,验证代码是否来自可信模板。
            // !!!直接返回用户提供的代码是非常危险的!!!
            if (code === "alert('Hello from trusted script!');") {
                return code;
            }
            console.error('Untrusted script code blocked:', code);
            throw new Error('Untrusted script code');
        },
        createHTML: (input) => {
            throw new Error('myScriptPolicy does not support createHTML');
        }
        // ... 其他类型
    });
} else {
    myScriptPolicy = {
        createScript: (code) => code,
        createHTML: (input) => input,
        createScriptURL: (input) => input,
        createURL: (input) => input,
        createStyle: (input) => input
    };
}

// 如何使用:
const safeCode = "alert('Hello from trusted script!');";
const maliciousCode = "alert('XSS via setTimeout!');";

try {
    if (myScriptPolicy) {
        // 使用策略创建 TrustedScript 对象
        setTimeout(myScriptPolicy.createScript(safeCode), 100);
        // 这将抛出错误,因为 maliciousCode 不在白名单中
        // setTimeout(myScriptPolicy.createScript(maliciousCode), 100);
    } else {
        setTimeout(safeCode, 100);
    }
} catch (e) {
    console.error('Failed to execute script with Trusted Types:', e);
}

重要提示createScript 策略是最难安全实现的。通常,最佳实践是避免在应用中使用 eval()setTimeout(string, ...)。如果非用不可,策略必须极其严格,例如,只允许执行经过预编译或已知安全的代码模板。

默认策略 trustedTypes.defaultPolicy

如果应用程序中的某个DOM接收器需要一个 Trusted Type 对象,但没有为该接收器显式地提供一个由 trustedTypes.createPolicy() 创建的策略,浏览器会尝试使用一个“默认策略”。你可以通过 trustedTypes.createPolicy('default', {...}) 来定义这个默认策略。

如果未定义默认策略,并且在 require-trusted-types-for 启用的情况下,将普通字符串赋给危险接收器,浏览器会直接抛出 TypeError

默认策略在某些情况下可能有用,例如,当第三方库无法立即兼容 Trusted Types 时,可以提供一个临时性的降级方案。然而,定义一个过于宽松的默认策略会显著削弱 Trusted Types 的保护作用,因此应谨慎使用。

// 定义一个默认策略
if (window.trustedTypes && trustedTypes.createPolicy) {
    trustedTypes.createPolicy('default', {
        createHTML: (input) => {
            console.warn('Default policy used for createHTML. Consider specific policies.');
            return DOMPurify.sanitize(input); // 仍然推荐净化
        },
        createScriptURL: (url) => {
            console.warn('Default policy used for createScriptURL. Consider specific policies.');
            // 默认策略通常应该非常保守,甚至直接拒绝
            if (new URL(url).origin === window.location.origin) {
                return url; // 仅允许同源 URL
            }
            throw new Error('Default policy blocked untrusted script URL');
        },
        createScript: (input) => {
            console.warn('Default policy used for createScript. This is highly discouraged.');
            throw new Error('Default policy blocked dynamic script execution');
        },
        createURL: (url) => {
            console.warn('Default policy used for createURL. Consider specific policies.');
            // 仅允许 http/https/mailto 协议,禁止 javascript:
            if (url.startsWith('http:') || url.startsWith('https:') || url.startsWith('mailto:')) {
                return url;
            }
            throw new Error('Default policy blocked untrusted URL protocol');
        }
    });
}

实际操作:迁移现有代码与受影响的DOM API

一旦 Trusted Types 在CSP中启用,所有向危险DOM接收器赋值普通字符串的代码都将立即中断。因此,迁移现有代码是采用 Trusted Types 的关键一步。这通常涉及到识别所有受影响的DOM API,并用策略函数创建的 Trusted Type 对象来替换普通字符串赋值。

Trusted Types 影响的主要 DOM API

下表列出了常见的DOM API及其在启用 Trusted Types 后所期望的 Trusted Type 类型:

DOM API / 属性 期望的 Trusted Type 对象 描述
Element.innerHTML TrustedHTML 设置元素的HTML内容。
Element.outerHTML TrustedHTML 替换整个元素或其内容。
Element.insertAdjacentHTML() TrustedHTML 在指定位置插入HTML内容。
Document.write() / Document.writeln() TrustedHTML 将HTML字符串写入文档流。
Element.setAttribute('src', value) (for <script>, <iframe>, <img>, etc.) TrustedScriptURL (for script.src), TrustedURL (for iframe/img src) 设置元素的 src 属性。
Element.setAttribute('href', value) (for <a>, <link>) TrustedURL 设置链接或样式表的 href 属性。
Element.setAttribute('formaction', value) TrustedURL 设置表单提交的目标URL。
Element.setAttribute('baseURI', value) TrustedURL 设置文档的基URI。
Element.setAttribute('data', value) (for <object>) TrustedURL 设置 object 元素的资源URL。
Element.setAttribute('style', value) TrustedStyle 设置元素的内联样式,如果值包含 url() 或其他危险内容。
CSSStyleSheet.insertRule(rule) TrustedStyle 插入CSS规则,如果规则字符串包含 url() 或其他危险内容。
window.location.href / window.location.assign() / window.location.replace() TrustedURL 导航到新的URL。
setTimeout(code, delay) TrustedScript 如果 code 是字符串,则执行该脚本。
setInterval(code, delay) TrustedScript 如果 code 是字符串,则周期性执行该脚本。
new Function(args, body) TrustedScript 从字符串创建函数。
eval(code) TrustedScript 执行字符串作为JavaScript代码。
XMLHttpRequest.open(method, url) TrustedURL 打开一个HTTP请求。
Response.redirect(url) (Service Worker) TrustedURL Service Worker 中的重定向。
Worker(scriptURL) / SharedWorker(scriptURL) TrustedScriptURL 创建新的 Worker。
import(moduleSpecifier) (Dynamic Import) TrustedScriptURL 动态导入模块。

迁移代码示例

假设我们已经创建了 myHTMLPolicymyScriptURLPolicymyURLPolicy

// 假设这些策略已经按前面示例创建和初始化
// myHTMLPolicy = trustedTypes.createPolicy('myHTMLPolicy', { createHTML: DOMPurify.sanitize });
// myScriptURLPolicy = trustedTypes.createPolicy('myScriptURLPolicy', { createScriptURL: validateScriptUrl });
// myURLPolicy = trustedTypes.createPolicy('myURLPolicy', { createURL: validateUrl });

// 示例1: innerHTML
const userGeneratedContent = "<p>User input with potentially dangerous <script>alert(1)</script> tags.</p>";
const outputDiv = document.getElementById('output');

if (myHTMLPolicy) {
    outputDiv.innerHTML = myHTMLPolicy.createHTML(userGeneratedContent);
} else {
    // 降级处理,不推荐在生产环境中使用
    outputDiv.innerHTML = userGeneratedContent;
}

// 示例2: script.src
const scriptUrl = "/path/to/safe_analytics.js";
const newScript = document.createElement('script');

if (myScriptURLPolicy) {
    newScript.src = myScriptURLPolicy.createScriptURL(scriptUrl);
} else {
    newScript.src = scriptUrl;
}
document.head.appendChild(newScript);

// 示例3: a.href (使用 setAttribute)
const userLinkInput = "https://example.com/safe-page";
// const userLinkInput = "javascript:alert('evil!')"; // 会被 myURLPolicy 阻止
const linkElement = document.createElement('a');
linkElement.textContent = "Click me";

if (myURLPolicy) {
    linkElement.setAttribute('href', myURLPolicy.createURL(userLinkInput));
} else {
    linkElement.setAttribute('href', userLinkInput);
}
document.body.appendChild(linkElement);

// 示例4: window.location.href
const redirectUrl = "/dashboard";
// const redirectUrl = "javascript:alert('redirect evil!')"; // 会被 myURLPolicy 阻止

if (myURLPolicy) {
    // 实际的重定向操作应该在用户交互后触发,这里仅作示例
    // window.location.href = myURLPolicy.createURL(redirectUrl);
} else {
    // window.location.href = redirectUrl;
}

// 示例5: setTimeout (假设有一个 myScriptPolicy)
// const myScriptPolicy = trustedTypes.createPolicy('myScriptPolicy', { createScript: (code) => { /* ... */ return code; } });
const functionToExecute = "console.log('Delayed execution from trusted source.');";

if (myScriptPolicy) {
    setTimeout(myScriptPolicy.createScript(functionToExecute), 1000);
} else {
    setTimeout(functionToExecute, 1000);
}

避免常见陷阱

  • 过于宽松的策略:最严重的错误是编写一个直接返回输入的策略函数,例如 createHTML: (input) => input。这完全绕过了 Trusted Types 的保护机制。
  • 不一致的策略:确保为所有相关的DOM接收器提供正确的 Trusted Type 类型。例如,不要尝试将 TrustedScriptURL 赋给 innerHTML
  • 第三方库兼容性:第三方库可能没有内置 Trusted Types 支持。这需要:
    • 等待库提供官方支持。
    • 为库使用的危险DOM API包装器编写自定义适配器策略。
    • 在极端情况下,如果无法修改库或适配,可能需要使用 trustedTypes.defaultPolicy 作为临时方案,但这会降低安全性。一些现代框架(如Angular)已经开始集成 Trusted Types 支持。
  • 忽略报告:在报告模式下,要积极监控CSP违规报告,它们是发现需要修复的代码的宝贵资源。

Trusted Types 的优势与效益

采用 Trusted Types 机制,为Web应用带来了显著的安全优势:

  1. 彻底根除 DOM-based XSS:这是最核心的优势。通过在浏览器渲染引擎层面强制进行类型检查,Trusted Types 能够确保只有经过应用程序明确信任和验证的数据才能进入敏感的DOM接收器。这使得开发者能够自信地抵御大多数复杂的DOM-based XSS攻击,即使传统CSP无法覆盖这些场景。
  2. 强制安全编码实践Trusted Types 强制开发者在每一次可能导致XSS的DOM操作时,都必须显式地思考数据的来源和安全性。这种显式的安全“屏障”促使开发者养成更好的安全编码习惯,从根本上减少了引入XSS漏洞的可能性。
  3. 提高代码可审计性与可维护性:由于所有危险操作都必须通过命名的策略函数进行,安全审计人员可以更容易地识别和审查这些策略的实现,确保它们是安全的。同时,集中管理安全策略也使得代码更易于维护和更新。
  4. 互补传统CSP,构建深度防御Trusted Types 并非取代CSP,而是对其能力的强大补充。CSP限制了 哪些资源 可以被加载,而 Trusted Types 则限制了 哪些数据 可以被插入到DOM中。两者结合,形成了Web应用更深层次的防御体系。即使CSP因某些原因(如 unsafe-inline)被削弱,Trusted Types 也能提供额外的保护。
  5. 减少对 unsafe-inline 的依赖:许多开发者被迫在CSP中启用 script-src 'unsafe-inline' 以支持遗留代码或内联事件处理。Trusted Types 通过提供结构化的方式来处理动态脚本和HTML内容,使得开发者有能力逐渐移除 unsafe-inline,从而显著增强CSP的安全性。
  6. 提前发现漏洞:在开发和测试阶段,Trusted Types 的严格类型检查可以在运行时立即抛出错误,帮助开发者在部署前发现并修复潜在的XSS漏洞。报告模式也提供了持续的反馈机制。
  7. 未来前端安全标准Trusted Types 是Web平台安全发展的重要方向,未来将有更多工具和框架集成其支持。及早采纳有助于应用保持与最新安全标准的同步。

挑战与注意事项

尽管 Trusted Types 带来了巨大的安全效益,但在实际部署和应用过程中,也存在一些挑战和需要注意的事项:

  1. 迁移成本:对于大型、复杂的现有应用程序而言,迁移到 Trusted Types 可能需要大量的工作。所有向危险DOM接收器赋值普通字符串的代码都需要被识别并修改,以使用 Trusted Type Policy。这通常是一个渐进式的过程,需要细致的规划和测试。
  2. 第三方库兼容性:这是最大的挑战之一。许多流行的第三方库和框架(尤其是那些直接操作DOM的)可能尚未原生支持 Trusted Types。这意味着你可能需要:
    • 等待库的更新。
    • 为这些库编写适配器层,拦截其DOM操作并应用 Trusted Types 策略。
    • 在无法避免的情况下,使用 trustedTypes.defaultPolicy,但这会削弱保护。
  3. 策略的正确性与安全性:策略函数是信任的边界。如果策略本身存在漏洞(例如,净化不彻底,或者允许过于宽泛的URL),那么整个 Trusted Types 机制就会被绕过。编写健壮、安全的策略需要专业的安全知识和细致的审查。
  4. 性能考量:虽然通常微不足道,但策略函数在每次危险DOM操作时都会执行。如果策略函数包含复杂的净化逻辑,可能会对性能产生轻微影响。应优化策略函数,使其尽可能高效。
  5. 浏览器支持:虽然主流现代浏览器(Chrome、Edge、Firefox等)已经支持 Trusted Types,但仍有一些旧版浏览器或特定环境可能不支持。在部署时需要考虑目标用户群的浏览器兼容性。
  6. 学习曲线:对于不熟悉 Trusted Types 概念的开发者来说,理解和正确实现它需要一定的学习过程。

展望 Web 安全的未来

Trusted Types 机制的引入,标志着Web平台在对抗XSS攻击方面迈出了关键一步。它将安全防线从宏观的资源加载层面,深入到了微观的DOM操作层面,实现了更细粒度的控制和更强大的保护。

作为编程专家,我们应该认识到,Web安全是一个持续演进的战场。没有银弹,只有多层次、深度的防御体系才能最大程度地保护我们的用户和应用。Trusted Types 与CSP、HTTP Strict Transport Security (HSTS)、Subresource Integrity (SRI) 等机制共同构成了现代Web安全的坚实基础。

未来,我们可以预见 Trusted Types 将被更广泛地集成到Web框架、组件库和开发工具中,使得开发者能够更轻松、更自然地构建安全的应用。我们鼓励所有的开发者积极学习和采纳 Trusted Types,共同推动Web安全实践向前发展。

结语

Trusted Types 机制为根除DOM-based XSS注入提供了一个强大而有效的解决方案。通过在DOM层面强制执行类型安全,它极大地提升了Web应用的抗攻击能力,并促进了更安全的编码实践。拥抱 Trusted Types,是构建未来安全、健壮Web应用的关键一步。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注