Trusted Types 实战:用浏览器原生 API 根治 DOM XSS 攻击

Trusted Types 是浏览器原生的 DOM XSS 防御 API,Chrome 已强制启用。本文从原理到实战,详解如何用 Trusted Types 策略模式彻底消除 innerHTML 注入风险,附完整迁移方案和框架集成代码。

安全与密码 2026-05-31 15 分钟

跨站脚本攻击(XSS)连续 15 年稳居 OWASP Top 10,而 DOM XSS 是其中最难防御的变种——攻击代码从未经过服务端,直接在浏览器 DOM 中触发。2024 年 Chrome 123 开始对未使用 Trusted Types 的页面发出控制台警告,2026 年初 Chrome 已在高安全级别下强制执行 Trusted Types 策略。如果你的前端代码还在直接操作 innerHTML,是时候了解一下这个浏览器原生的安全防线了。

🔐 一、DOM XSS 的本质与 Trusted Types 的解法

1.1 为什么 DOM XSS 如此难防?

传统 XSS(反射型、存储型)可以通过服务端转义来防御,但 DOM XSS 的攻击路径完全不同:恶意数据从 URL hash、document.referrerpostMessage 等客户端来源直接进入 DOM 操作函数,服务端根本看不到这段数据。

最常见的 DOM XSS 触发点:

// ❌ 危险写法:直接将用户输入插入 DOM
element.innerHTML = location.hash.slice(1);        // URL 片段注入
element.innerHTML = localStorage.getItem('name');   // 存储型 DOM XSS
document.write(new URLSearchParams(location.search).get('q'));  // 查询参数注入

根据 Google 的统计数据,超过 60% 的 XSS 漏洞源于 innerHTMLdocument.write 的不安全使用。传统的 CSP(Content Security Policy)可以限制脚本来源,但无法阻止这些 DOM API 被滥用——因为恶意代码是通过合法的 JavaScript 执行的。

1.2 Trusted Types 的核心思想

Trusted Types 的解法非常优雅:不禁止危险 API,而是要求所有传入的数据必须经过「可信策略」处理后才能使用

具体来说,它做了三件事:

  1. 创建安全策略(Policy):定义一个数据处理函数,所有数据必须经过这个函数才能变成「可信类型」
  2. 包装危险 APIinnerHTMLouterHTMLdocument.write 等 API 被重写,只接受 TrustedHTML 类型的数据
  3. 强制执行(Enforcement):通过 CSP 头 require-trusted-types-for 'script' 启用强制模式,违规直接报错

💡 **提示:**Trusted Types 不是一个 npm 库,而是浏览器原生 API(Chrome、Edge 已全面支持,Firefox 和 Safari 正在实现中)。它不需要任何运行时依赖。

1.3 类型体系

Trusted Types 定义了三种可信类型,分别对应不同的 DOM 操作:

类型 对应 API 用途
TrustedHTML innerHTML, outerHTML, insertAdjacentHTML HTML 片段插入
TrustedScript script.src, script.text, eval() 脚本执行
TrustedScriptURL script.src, iframe.src, fetch() URL 加载

⚡ **关键结论:**Trusted Types 的核心理念是「类型即安全」——通过类型系统在运行时强制执行数据清洗,让 innerHTML 注入从「可能忘记转义」变成「不转义就报错」。

🚀 二、从零实现:Trusted Types 策略实战

2.1 基础策略创建

下面是一个完整的 Trusted Types 策略实现,包含 HTML 转义和 URL 校验:

// 创建 Trusted Types 策略:统一处理所有 DOM 插入的数据
const securityPolicy = trustedTypes.createPolicy('default', {
  // 处理 HTML 插入:转义所有特殊字符
  createHTML(input) {
    const div = document.createElement('div');
    div.textContent = String(input);  // textContent 自动转义 HTML 实体
    return div.innerHTML;
  },

  // 处理脚本:只允许已知的安全脚本
  createScript(input) {
    const allowed = ['console.log', 'window.location.reload'];
    const str = String(input);
    if (!allowed.some(fn => str.startsWith(fn))) {
      throw new Error(`🚫 脚本不在白名单中: ${str.slice(0, 50)}`);
    }
    return str;
  },

  // 处理脚本 URL:只允许同源和可信域名
  createScriptURL(input) {
    const url = new URL(String(input), location.origin);
    const trustedHosts = ['cdn.example.com', 'apis.google.com', location.hostname];
    if (!trustedHosts.includes(url.hostname)) {
      throw new Error(`🚫 不可信的脚本源: ${url.hostname}`);
    }
    return url.href;
  }
});

// ✅ 安全使用:通过策略创建可信类型
const userInput = '<img src=x onerror=alert("XSS")>';
document.getElementById('content').innerHTML = securityPolicy.createHTML(userInput);
// 输出: &lt;img src=x onerror=alert("XSS")&gt; (已转义,不会执行)

2.2 多策略架构:按功能分离职责

生产环境中,不应该把所有逻辑塞进一个策略。推荐按功能模块创建独立策略:

// 模板渲染策略:处理 UI 模板中的动态数据
const templatePolicy = trustedTypes.createPolicy('template', {
  createHTML(input) {
    // 允许安全的 HTML 标签,过滤危险属性
    return sanitizeHTML(String(input), {
      allowedTags: ['b', 'i', 'em', 'strong', 'a', 'span', 'p', 'br'],
      allowedAttributes: {
        'a': ['href', 'title'],
        'span': ['class'],
      },
      // 禁止所有 on* 事件处理器
      disallowedAttributesMode: 'discard',
    });
  }
});

// 动态脚本策略:用于第三方分析和 A/B 测试
const analyticsPolicy = trustedTypes.createPolicy('analytics', {
  createScriptURL(input) {
    const url = new URL(String(input), location.origin);
    // 只允许 Google Analytics 和自研埋点服务
    const allowed = ['google-analytics.com', 'analytics.example.com'];
    if (!allowed.some(host => url.hostname.endsWith(host))) {
      throw new Error(`🚫 不允许的分析脚本: ${url.hostname}`);
    }
    return url.href;
  }
});

// 富文本编辑器策略:允许更多 HTML 标签
const editorPolicy = trustedTypes.createPolicy('rich-editor', {
  createHTML(input) {
    return sanitizeHTML(String(input), {
      allowedTags: ['b', 'i', 'u', 'h1', 'h2', 'h3', 'p', 'ul', 'ol', 'li',
                    'blockquote', 'code', 'pre', 'a', 'img', 'table', 'tr', 'td', 'th'],
      allowedAttributes: {
        'a': ['href', 'title', 'target'],
        'img': ['src', 'alt', 'width', 'height'],
        'code': ['class'],
      },
    });
  }
});

⚠️ **警告:**每个策略名称只能注册一次。如果两个模块使用相同名称创建策略,第二个会抛出 TypeError。建议用模块名作为策略名称前缀。

2.3 与 DOMPurify 集成

DOMPurify 是最流行的 HTML 清理库,它已经内置了 Trusted Types 支持:

// DOMPurify + Trusted Types:生产级 HTML 清理方案
import DOMPurify from 'dompurify';

// 创建与 DOMPurify 集成的策略
const purifyPolicy = trustedTypes.createPolicy('dompurify', {
  createHTML(dirty) {
    // DOMPurify 返回清理后的 HTML 字符串
    // 返回值自动被包装为 TrustedHTML 类型
    return DOMPurify.sanitize(dirty, {
      ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br', 'ul', 'ol', 'li'],
      ALLOWED_ATTR: ['href', 'title', 'target', 'class'],
      ALLOW_DATA_ATTR: false,
      // 防止 CSS 注入
      FORBID_ATTR: ['style'],
      // 防止 DOM Clobbering
      SANITIZE_DOM: true,
    });
  }
});

// 使用示例
function renderUserComment(html) {
  // ✅ 安全:经过 DOMPurify 清理 + Trusted Types 类型保证
  commentContainer.innerHTML = purifyPolicy.createHTML(html);
}

// ❌ 在强制模式下,直接赋值会抛出 TypeError
// commentContainer.innerHTML = '<script>alert("XSS")</script>';
// TypeError: Failed to set the 'innerHTML' property: This document requires 'TrustedHTML' assignment.

📌 **记住:**DOMPurify 的 RETURN_TRUSTED_TYPE 选项可以让它直接返回 TrustedHTML,但为了更好的审计和调试,推荐显式创建策略并在策略中调用 DOMPurify。

💡 三、生产部署:CSP 配置与渐进式迁移

3.1 CSP 头配置

Trusted Types 的强制执行通过 CSP 头控制。以下是一个完整的生产级配置:

# Nginx 配置示例
# 阶段 1:报告模式(不阻断,只报告违规)
add_header Content-Security-Policy "
  require-trusted-types-for 'script';
  trusted-types default template analytics dompurify;
  report-uri /csp-violation-report;
  script-src 'self' https://cdn.example.com;
  style-src 'self' 'unsafe-inline';
";

# 阶段 2:强制模式(阻断所有违规)
# 以下配置在迁移完成后启用
add_header Content-Security-Policy "
  require-trusted-types-for 'script';
  trusted-types default template analytics dompurify;
  script-src 'self' https://cdn.example.com;
  style-src 'self' 'unsafe-inline';
";

CSP 指令说明:

指令 作用 示例
require-trusted-types-for 'script' 启用强制模式,危险 API 必须使用可信类型 'script' 是目前唯一支持的值
trusted-types 声明允许的策略名称列表 default template analytics
trusted-types 'allow-duplicates' 允许同名策略重复创建 第三方库兼容
report-uri 违规报告地址(报告模式用) /csp-violation-report

⚡ **关键结论:**迁移分为三个阶段——报告模式收集违规、逐个修复代码、最终切换到强制模式。不要试图一步到位,大型项目通常需要 2-4 周完成迁移。

3.2 渐进式迁移策略

对于已有的大型项目,推荐以下迁移路径:

阶段 1:审计与报告(1 周)

// 安装报告处理器,收集所有违规
// server.js — Express 违规报告端点
app.post('/csp-violation-report', express.json(), (req, res) => {
  const violation = req.body['csp-report'];
  console.warn('🚨 CSP Violation:', {
    blocked: violation['blocked-uri'],
    violated: violation['violated-directive'],
    source: violation['source-file'],
    line: violation['line-number'],
  });

  // 存储到数据库,分析高频违规点
  saveViolation(violation);
  res.status(204).end();
});

阶段 2:创建默认策略兜底(2-3 天)

// 在应用入口文件最顶部创建默认策略
// ⚠️ 必须在所有其他代码之前执行
if (typeof trustedTypes !== 'undefined') {
  trustedTypes.createPolicy('default', {
    createHTML(input) {
      // 默认策略:记录违规但不阻断
      console.warn('⚠️ 未通过策略处理的 HTML:', String(input).slice(0, 100));
      const div = document.createElement('div');
      div.textContent = String(input);
      return div.innerHTML;
    },
    createScript(input) {
      console.warn('⚠️ 未通过策略处理的 Script:', String(input).slice(0, 100));
      return String(input);
    },
    createScriptURL(input) {
      console.warn('⚠️ 未通过策略处理的 ScriptURL:', String(input).slice(0, 100));
      return String(input);
    }
  });
}

阶段 3:逐模块替换(1-3 周)

// 逐个模块替换 innerHTML 为安全策略
// ❌ 迁移前
function renderUserProfile(user) {
  profileEl.innerHTML = `
    <h2>${user.name}</h2>
    <p>${user.bio}</p>
    <a href="${user.website}">个人网站</a>
  `;
}

// ✅ 迁移后
const profilePolicy = trustedTypes.createPolicy('user-profile', {
  createHTML(input) {
    return DOMPurify.sanitize(String(input), {
      ALLOWED_TAGS: ['h2', 'p', 'a', 'span'],
      ALLOWED_ATTR: ['href'],
    });
  }
});

function renderUserProfile(user) {
  profileEl.innerHTML = profilePolicy.createHTML(`
    <h2>${escapeHtml(user.name)}</h2>
    <p>${escapeHtml(user.bio)}</p>
    <a href="${escapeUrl(user.website)}">个人网站</a>
  `);
}

3.3 框架集成

现代前端框架已经部分内置了 XSS 防护,但仍有绕过风险。以下是各框架的最佳实践:

Vue 3 + Trusted Types:

// vue-trusted-html.js — Vue 自定义指令
// 用于替代 v-html,强制经过 Trusted Types 处理
import { createPolicy } from './trusted-types-setup';

const vueHtmlPolicy = createPolicy('vue-html', {
  createHTML(input) {
    return DOMPurify.sanitize(input);
  }
});

export const vTrustedHtml = {
  mounted(el, binding) {
    el.innerHTML = vueHtmlPolicy.createHTML(binding.value);
  },
  updated(el, binding) {
    if (binding.value !== binding.oldValue) {
      el.innerHTML = vueHtmlPolicy.createHTML(binding.value);
    }
  }
};

// 使用:<div v-trusted-html="userContent"></div>

React 中的 Trusted Types:

// React 本身对 dangerouslySetInnerHTML 有警告
// 结合 Trusted Types 可以做到运行时强制防护
const trustedHtmlPolicy = trustedTypes.createPolicy('react-html', {
  createHTML(input) {
    return DOMPurify.sanitize(input, {
      ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],
      ALLOWED_ATTR: ['href', 'target'],
    });
  }
});

function SafeRichText({ content }) {
  // ✅ 通过策略处理后的 HTML
  return (
    <div
      dangerouslySetInnerHTML={{
        __html: trustedHtmlPolicy.createHTML(content)
      }}
    />
  );
}

// ❌ 直接使用会触发 CSP 违规
// <div dangerouslySetInnerHTML={{ __html: userInput }} />

🔧 四、常见坑点与避坑指南

4.1 第三方库兼容性问题

最大的痛点是第三方库可能直接操作 innerHTML。解决方案:

// 在加载第三方库之前创建默认策略
// 这样第三方库的 innerHTML 操作会经过你的策略处理
trustedTypes.createPolicy('third-party-default', {
  createHTML(input) {
    // 对第三方库的 HTML 做基本清理
    return DOMPurify.sanitize(String(input), {
      // 根据第三方库的需求调整白名单
      ADD_TAGS: ['svg', 'path', 'use'],  // 图标库需要
      ADD_ATTR: ['viewBox', 'd', 'fill'], // SVG 属性
    });
  }
});

4.2 调试技巧

// 开发环境下注册违规监听器
if (process.env.NODE_ENV === 'development') {
  document.addEventListener('securitypolicyviolation', (e) => {
    console.group('🔒 Trusted Types Violation');
    console.error('Blocked:', e.blockedURI);
    console.error('Directive:', e.violatedDirective);
    console.error('Source:', e.sourceFile, `:${e.lineNumber}`);
    console.error('Original:', e.originalPolicy);
    console.groupEnd();
  });
}

4.3 性能影响

场景 无 Trusted Types 有 Trusted Types 开销
简单文本插入 0.001ms 0.003ms +0.002ms
复杂 HTML(1KB) 0.05ms 0.12ms +0.07ms
DOMPurify 清理 0.8ms 0.85ms +0.05ms
1000 次批量插入 45ms 52ms +15%

⚠️ **警告:**Trusted Types 的性能开销在绝大多数场景下可以忽略。唯一需要注意的是在高频动画或 requestAnimationFrame 循环中避免使用策略函数——这种场景应该预先处理好数据。

✅ 总结与建议

Trusted Types 不是银弹,但它是目前唯一能在运行时强制阻止 DOM XSS 的浏览器原生方案。相比传统的「代码审查 + ESLint 规则」,它的优势在于:即使开发者犯了错误,CSP 强制模式也会直接阻断注入。

实施建议:

  • ✅ 新项目从第一天就启用 Trusted Types + CSP 强制模式
  • ✅ 已有项目先用报告模式收集数据,再渐进式迁移
  • ✅ 将 Trusted Types 策略与 DOMPurify 结合使用
  • ✅ 用 securitypolicyviolation 事件做开发期调试
  • ❌ 不要创建过于宽松的默认策略(失去了防护意义)
  • ❌ 不要忽略第三方库的兼容性问题

相关资源:

📌 **记住:**安全不是一次性工程。Trusted Types + CSP + DOMPurify 组成了前端 XSS 防御的三道防线,缺一不可。定期审计 CSP 违规报告,持续迭代安全策略,才是真正的生产级安全实践。

📚 相关文章