强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

第 16 章:安全策略(Security)

第 16 章:安全策略(Security)

安全是 Chrome 扩展开发中不可忽视的核心话题。扩展拥有比普通网页更高的权限,一旦存在安全漏洞,后果严重。本章将全面讲解扩展的安全模型、内容安全策略(CSP)、沙箱机制和安全编码最佳实践。


16.1 扩展安全模型

16.1.1 安全架构

┌───────────────────────────────────────────────────────┐
│                   Chrome 浏览器                        │
│                                                        │
│  ┌────────────────────┐  ┌─────────────────────────┐  │
│  │  扩展上下文          │  │  网页上下文              │  │
│  │  (Privileged)       │  │  (Unprivileged)         │  │
│  │                     │  │                         │  │
│  │  Service Worker     │  │  页面 JS                │  │
│  │  Popup / Options    │  │  页面 DOM               │  │
│  │  Side Panel         │  │                         │  │
│  │                     │  │                         │  │
│  │  ✅ chrome.* API    │  │  ❌ chrome.* API        │  │
│  │  ✅ 跨域请求        │  │  ❌ 受同源策略限制       │  │
│  │  ✅ 访问浏览器数据   │  │  ❌ 无法访问            │  │
│  └────────┬───────────┘  └────────────┬────────────┘  │
│           │                           │                │
│           │    ┌───────────────┐      │                │
│           │    │Content Script │      │                │
│           └────┤ (有限权限)    ├──────┘                │
│                │               │                       │
│                │ ✅ DOM 访问   │                       │
│                │ ✅ chrome.*   │                       │
│                │ ❌ 页面变量   │                       │
│                └───────────────┘                       │
│                                                        │
│  ┌──────────────────────────────────────────────────┐  │
│  │              沙箱 (Sandbox)                       │  │
│  │  ┌─────────────────────────────────────────────┐ │  │
│  │  │  sandboxed pages                             │ │  │
│  │  │  ❌ 无 chrome.* API                          │ │  │
│  │  │  ❌ 无 DOM 访问(受限)                       │ │  │
│  │  │  ✅ 安全运行不受信任的代码                    │ │  │
│  │  └─────────────────────────────────────────────┘ │  │
│  └──────────────────────────────────────────────────┘  │
└───────────────────────────────────────────────────────┘

16.1.2 Manifest V3 安全改进

MV3 安全措施说明
禁止远程代码所有代码必须打包在扩展中
Service Worker 限制无 DOM 访问,减少 XSS 风险
声明式网络请求无法读取请求内容
CSP 严格化不允许 'unsafe-eval''unsafe-inline'
可选权限最小权限原则

16.2 内容安全策略(CSP)

16.2.1 默认 CSP

MV3 扩展的默认 CSP:

{
  "content_security_policy": {
    "extension_pages": "script-src 'self'; object-src 'self'"
  }
}

这意味着:

  • 只能执行扩展包内的脚本
  • 只能加载扩展包内的对象(如 Flash、Java)
  • 不允许内联脚本(<script>...</script>
  • 不允许 eval() 和类似函数
  • 不允许远程脚本加载

16.2.2 CSP 配置选项

{
  "content_security_policy": {
    "extension_pages": "script-src 'self'; object-src 'self'",
    "sandbox": "sandbox allow-scripts; script-src 'self'"
  }
}
CSP 指令可配置值说明
script-src'self'仅允许扩展内脚本
object-src'self', 'none'控制插件加载
sandboxsandbox 属性沙箱页面的策略

🚫 警告:MV3 中不允许使用 'unsafe-eval''unsafe-inline' 或远程脚本 URL。

16.2.3 CSP 合规编码

// ❌ 不允许 — 内联脚本
// <button onclick="handleClick()">Click</button>

// ✅ 正确 — 使用 addEventListener
document.getElementById('btn').addEventListener('click', handleClick);

// ❌ 不允许 — eval()
const fn = new Function('return ' + userInput);

// ✅ 正确 — 使用安全的替代方案
const result = JSON.parse(jsonString);

// ❌ 不允许 — 内联样式(注意:style 属性可能仍可用,但推荐外部样式)
// <div style="color: red">

// ✅ 正确 — 使用外部 CSS 或 classList
element.classList.add('error-style');

// ❌ 不允许 — 字符串模板生成 HTML
element.innerHTML = `<div onclick="${handler}">...</div>`;

// ✅ 正确 — 使用 DOM API 创建元素
const div = document.createElement('div');
div.textContent = '...';
div.addEventListener('click', handler);

16.3 Cross-Site Scripting (XSS) 防御

16.3.1 常见 XSS 攻击向量

// 危险:直接插入用户输入
document.getElementById('output').innerHTML = userInput;

// 危险:从 URL 参数插入
const params = new URLSearchParams(window.location.search);
document.body.innerHTML = params.get('name');

// 危险:从存储中读取并直接渲染
const { template } = await chrome.storage.local.get('template');
container.innerHTML = template;

16.3.2 安全的 DOM 操作

// 安全的文本插入
function safeSetText(element, text) {
  element.textContent = text; // 自动转义
}

// 安全的 HTML 构建
function safeCreateElement(tag, attrs = {}, children = []) {
  const el = document.createElement(tag);

  for (const [key, value] of Object.entries(attrs)) {
    if (key === 'textContent') {
      el.textContent = value;
    } else if (key.startsWith('on')) {
      // 不允许内联事件处理器
      console.warn('Inline event handlers are not allowed');
    } else {
      el.setAttribute(key, String(value));
    }
  }

  for (const child of children) {
    if (typeof child === 'string') {
      el.appendChild(document.createTextNode(child));
    } else if (child instanceof Node) {
      el.appendChild(child);
    }
  }

  return el;
}

// 使用示例
const card = safeCreateElement('div', { class: 'card' }, [
  safeCreateElement('h2', { textContent: userInput }),
  safeCreateElement('p', { textContent: description }),
  safeCreateElement('a', {
    href: sanitizeUrl(url),
    textContent: '了解更多'
  })
]);

document.getElementById('container').appendChild(card);

16.3.3 URL 净化

function sanitizeUrl(url) {
  try {
    const parsed = new URL(url);

    // 只允许 http/https 协议
    if (!['http:', 'https:'].includes(parsed.protocol)) {
      return 'about:blank';
    }

    return parsed.href;
  } catch {
    return 'about:blank';
  }
}

// 检测 JavaScript: URL
function isDangerousUrl(url) {
  const lower = url.toLowerCase().trim();
  return lower.startsWith('javascript:') ||
         lower.startsWith('data:') ||
         lower.startsWith('vbscript:');
}

16.4 沙箱页面

16.4.1 沙箱页面配置

{
  "sandbox": {
    "pages": [
      "sandbox/editor.html",
      "sandbox/preview.html"
    ]
  },
  "content_security_policy": {
    "sandbox": "sandbox allow-scripts; script-src 'self'; style-src 'self' 'unsafe-inline'"
  }
}

16.4.2 沙箱页面用途

<!-- sandbox/editor.html -->
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>Rich Text Editor</title>
  <link rel="stylesheet" href="editor.css">
</head>
<body>
  <div id="editor" contenteditable="true"></div>
  <script src="editor.js"></script>
</body>
</html>
// sandbox/editor.js
// 沙箱中可以安全运行用户提供的 HTML 模板

function renderUserTemplate(template, data) {
  // 在沙箱中渲染用户自定义模板
  // 即使模板有恶意代码,也无法访问 chrome.* API
  const container = document.getElementById('editor');

  try {
    // 简单模板引擎
    let html = template;
    for (const [key, value] of Object.entries(data)) {
      html = html.replace(
        new RegExp(`{{${key}}}`, 'g'),
        escapeHtml(String(value))
      );
    }
    container.innerHTML = html;
  } catch (error) {
    container.textContent = '模板渲染失败';
  }
}

function escapeHtml(text) {
  const div = document.createElement('div');
  div.textContent = text;
  return div.innerHTML;
}

// 与扩展上下文通信
window.addEventListener('message', (event) => {
  if (event.data.type === 'render') {
    renderUserTemplate(event.data.template, event.data.data);

    // 将渲染结果发回
    window.parent.postMessage({
      type: 'rendered',
      html: document.getElementById('editor').innerHTML
    }, '*');
  }
});

16.5 数据安全

16.5.1 敏感数据存储

// ❌ 不安全 — 将密码明文存储
await chrome.storage.local.set({ password: 'user123' });

// ✅ 更安全 — 使用 Web Crypto API 加密
async function encryptData(data, key) {
  const encoder = new TextEncoder();
  const iv = crypto.getRandomValues(new Uint8Array(12));

  const cryptoKey = await crypto.subtle.importKey(
    'raw', key, { name: 'AES-GCM' }, false, ['encrypt']
  );

  const encrypted = await crypto.subtle.encrypt(
    { name: 'AES-GCM', iv },
    cryptoKey,
    encoder.encode(JSON.stringify(data))
  );

  return {
    iv: Array.from(iv),
    data: Array.from(new Uint8Array(encrypted))
  };
}

async function decryptData(encrypted, key) {
  const cryptoKey = await crypto.subtle.importKey(
    'raw', key, { name: 'AES-GCM' }, false, ['decrypt']
  );

  const decrypted = await crypto.subtle.decrypt(
    { name: 'AES-GCM', iv: new Uint8Array(encrypted.iv) },
    cryptoKey,
    new Uint8Array(encrypted.data)
  );

  return JSON.parse(new TextDecoder().decode(decrypted));
}

// 生成加密密钥
async function generateKey() {
  const key = await crypto.subtle.generateKey(
    { name: 'AES-GCM', length: 256 },
    true,
    ['encrypt', 'decrypt']
  );

  return await crypto.subtle.exportKey('raw', key);
}

16.5.2 Token 管理

class TokenManager {
  constructor() {
    this.tokenKey = 'auth_token';
    this.refreshKey = 'refresh_token';
  }

  async saveTokens(accessToken, refreshToken, expiresIn) {
    await chrome.storage.session.set({
      [this.tokenKey]: accessToken,
      [this.refreshKey]: refreshToken,
      tokenExpiresAt: Date.now() + expiresIn * 1000
    });
  }

  async getAccessToken() {
    const { [this.tokenKey]: token, tokenExpiresAt } =
      await chrome.storage.session.get([this.tokenKey, 'tokenExpiresAt']);

    if (!token) return null;

    // 检查是否过期(提前 60 秒刷新)
    if (Date.now() > tokenExpiresAt - 60000) {
      return await this.refreshAccessToken();
    }

    return token;
  }

  async refreshAccessToken() {
    const { [this.refreshKey]: refreshToken } =
      await chrome.storage.session.get(this.refreshKey);

    if (!refreshToken) return null;

    try {
      const response = await fetch('https://auth.example.com/refresh', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ refresh_token: refreshToken })
      });

      const data = await response.json();
      await this.saveTokens(data.access_token, data.refresh_token, data.expires_in);
      return data.access_token;
    } catch {
      await this.clearTokens();
      return null;
    }
  }

  async clearTokens() {
    await chrome.storage.session.remove([
      this.tokenKey, this.refreshKey, 'tokenExpiresAt'
    ]);
  }
}

16.6 安全检查清单

开发阶段

安全检查清单 (开发阶段)

代码安全:
  □ 不使用 eval(), new Function(), setTimeout(string)
  □ 不使用 innerHTML 直接插入用户输入
  □ URL 参数经过净化处理
  □ 使用 textContent 而非 innerHTML 显示文本
  □ 事件处理器通过 addEventListener 绑定

存储安全:
  □ 敏感数据不存储在 localStorage
  □ Token 存储在 session storage
  □ 本地数据加密存储

通信安全:
  □ 验证消息发送者 (sender.id, sender.url)
  □ 外部消息验证来源域名
  □ 不通过消息传递敏感数据

权限安全:
  □ 仅申请必需的权限
  □ 使用 activeTab 替代 broad host permissions
  □ 可选权限运行时申请

网络安全:
  □ 所有 API 调用使用 HTTPS
  □ 验证 SSL 证书(fetch 默认验证)
  □ 不信任外部返回的数据

发布前检查

安全检查清单 (发布前)

静态分析:
  □ 运行 npm audit 检查依赖漏洞
  □ 检查是否有遗留的 console.log(可能泄露信息)
  □ 确认无内联脚本

动态检查:
  □ 测试 XSS 向量(注入 <script> 等)
  □ 测试权限边界
  □ 测试错误处理

合规检查:
  □ 隐私政策已更新
  □ 数据收集说明完整
  □ 权限使用说明清晰

16.7 常见安全漏洞

漏洞类型风险防御措施
XSS (跨站脚本)使用 textContent、DOM API、净化输入
权限提升最小权限原则、验证消息来源
数据泄露加密存储、不通过 URL 传递敏感数据
中间人攻击全部使用 HTTPS、验证响应
代码注入禁止 eval、禁止远程代码
点击劫持验证用户意图、使用确认对话框

16.8 扩展阅读