Hybrid之安全性

32 阅读13分钟

Hybrid之安全性

0. 引言

Hybrid 的核心价值是 “Web 快速迭代 + Native 能力增强”。但一旦把 WebView 与原生能力桥接在一起,安全边界会从单一的浏览器沙箱,扩展为:页面来源、运行时环境、桥接接口、设备权限、网络链路、本地存储、发布与灰度链路 的组合系统。

这篇内容以“可落地”为目标:从攻击面拆解到防护策略,再到验收清单与应急流程,尽量把每一条安全要求落到 可执行的规则、可验证的日志、可自动化的测试

1. 安全边界与威胁模型(Threat Model)

1.1 安全边界(你要保护什么)

  • 身份与会话:登录态、Token、Cookie、设备标识、风控凭证
  • 业务敏感数据:订单、资金、隐私字段、地理位置、通讯录等
  • 原生能力:相机/相册、定位、文件系统、剪贴板、通知、蓝牙、支付能力
  • 发布链路:页面资源(HTML/JS/CSS)、配置下发、热更新/离线包

1.2 典型威胁(威胁从哪里来)

  • 加载链路被劫持:DNS/HTTP 劫持、代理篡改、弱 TLS
  • 页面注入:XSS、第三方脚本污染、DOM Clobbering
  • JSBridge 被滥用:未校验来源/参数,导致越权调用原生能力
  • 本地数据泄露:Web Storage / 缓存 / 日志 / 截屏 / 备份
  • 调试与注入:调试开关泄露、注入框架、Hook、越狱/Root 环境
  • 供应链攻击:依赖库/SDK 被投毒、CDN 资源被替换

2. Hybrid 攻击面地图(Attack Surface Map)

2.1 攻击面分层

  • L0 加载层:URL / 离线包 / 路由 / 重定向 / scheme
  • L1 渲染层:DOM / JS 执行环境 / CSP / iframe
  • L2 桥接层:JSBridge 协议、能力注册、权限模型、参数校验
  • L3 网络层:TLS、证书校验、代理、重放、签名、时间窗
  • L4 存储层:Cookie、IndexedDB、localStorage、缓存、日志
  • L5 运行时层:调试、Hook、越狱/Root、进程注入、重打包

3. 安全设计总原则

3.1 最小暴露(Principle of Least Privilege)

  • 桥接接口最小化:只暴露业务必需能力,避免“万能方法”
  • 能力分级:读/写/支付/隐私能力区分风险等级
  • 默认拒绝:未命中白名单/未通过校验直接拒绝

3.2 可验证(Verifiability)

  • 每一次敏感能力调用都有审计日志(不含敏感明文)
  • 每一条安全规则可自动化测试(静态 + 动态)
  • 每一次线上异常可回溯(请求链路、桥接调用链)

3.3 分层防御(Defense in Depth)

  • Web 安全(CSP / SameSite / SRI)解决“页面侧”
  • Bridge 协议(来源校验 / 授权 / 签名)解决“能力侧”
  • 网络与证书(TLS / Pinning)解决“链路侧”
  • 运行时与发布(完整性 / 灰度开关)解决“环境侧”

4. 安全加载:来源控制 + 路由校验

4.1 目标

把“加载什么页面”变成一个 可控、可审计、可回滚 的决策过程:

  • 只允许受控来源(域名/路径/离线包签名)
  • 限制重定向(禁止跳出白名单)
  • 拒绝高风险 scheme(如 file:、未知自定义 scheme)

4.2 推荐流程(Mermaid)

flowchart TD
  A[接收待加载 URL] --> B{URL 语法合法?}
  B -- 否 --> X[拒绝并记录审计]
  B -- 是 --> C{scheme = https?}
  C -- 否 --> X
  C -- 是 --> D{命中域名/路径白名单?}
  D -- 否 --> X
  D -- 是 --> E{是否发生重定向?}
  E -- 否 --> F[加载页面]
  E -- 是 --> G[获取最终 URL]
  G --> D

4.3 精简代码示例:URL 白名单与重定向策略

下面示例把“允许加载的 URL”收敛为 域名 + 路径前缀 白名单,并显式拒绝 http:/file: 等 scheme。真实项目中应把白名单下沉到 Native 并配合配置签名与灰度。

const ALLOW = [
  { host: 'm.example.com', pathPrefix: '/hybrid/' },
  { host: 'static.example.com', pathPrefix: '/h5/' },
];

export function isAllowedUrl(input) {
  let u;
  try {
    u = new URL(input);
  } catch {
    return false;
  }
  if (u.protocol !== 'https:') return false;

  return ALLOW.some(({ host, pathPrefix }) =>
    u.host === host && u.pathname.startsWith(pathPrefix)
  );
}

5. Web 安全基线:CSP / Cookie / 资源完整性

5.1 CSP:把“可执行代码”限制在你可控的范围

在 Hybrid 中,CSP 是限制脚本注入面最有效的 Web 侧手段之一。建议优先使用:

  • script-src 使用 nonce(或 hash)
  • 逐步收紧 default-srcconnect-src
  • frame-ancestors 防点击劫持

5.2 精简代码示例:生成 CSP nonce 并拼装策略字符串

下面示例演示“每次响应生成一个 nonce,并把 nonce 同时用于 CSP 头与内联脚本”。(实际下发 CSP 更推荐由服务端响应头完成;Hybrid 离线包也可在模板构建阶段注入。)

import crypto from 'crypto';

export function buildCspHeader() {
  const nonce = crypto.randomBytes(16).toString('base64');
  const csp = [
    "default-src 'none'",
    `script-src 'nonce-${nonce}' 'strict-dynamic' https:`,
    "style-src 'self' 'unsafe-inline' https:",
    "img-src 'self' https: data:",
    "connect-src 'self' https:",
    "frame-ancestors 'none'",
    "base-uri 'none'",
  ].join('; ');
  return { nonce, csp };
}

5.3 Cookie:SameSite + Secure + HttpOnly

  • WebView 里 Cookie 仍可能被注入脚本读取(若非 HttpOnly)
  • 跨站请求重定向登录场景易误用 SameSite

建议:

  • 会话 Cookie:Secure; HttpOnly; SameSite=Lax/Strict(按业务跳转模型选择)
  • 关键动作:增加 二次校验(签名/挑战)而不是只依赖 Cookie

6. JSBridge 安全:协议、授权、签名与回放防护

6.1 设计目标

把“JS 调 Native”从一个随意的函数调用,升级为 有身份、有权限、有时效、有完整性校验 的受控通道。

  • 来源校验:只允许受信任页面调用
  • 能力授权:按能力、按页面、按用户态授权
  • 参数校验:强类型、白名单、长度限制
  • 防重放:nonce + timestamp + 短时间窗
  • 可观测:每次调用都有 traceId

6.2 推荐调用链(Mermaid:Sequence)

sequenceDiagram
  participant H5 as H5 页面
  participant Bridge as JSBridge
  participant Native as Native 容器

  H5->>Bridge: postMessage({method, params, nonce, ts, sig})
  Bridge->>Native: 转发请求
  Native->>Native: 校验来源/权限/参数/时间窗/签名
  alt 校验通过
    Native-->>Bridge: {ok:true, data, traceId}
    Bridge-->>H5: resolve(data)
  else 校验失败
    Native-->>Bridge: {ok:false, code, traceId}
    Bridge-->>H5: reject(code)
  end

6.3 精简代码示例:Bridge 消息封装 + 校验骨架

下面示例体现三个关键点:

  • 固定消息结构(便于 Native 侧做严格校验)
  • 时间窗(例如 30 秒)
  • 签名字段(sig)作为完整性校验占位:真实项目中 sig 的密钥应由 Native 安全持有,JS 只能拿到短期会话令牌或由 Native 代签。
const BRIDGE_WINDOW_MS = 30_000;

export function buildBridgePayload(method, params, { nonce, ts, sig }) {
  return {
    v: 1,
    method,
    params,
    nonce,
    ts,
    sig,
  };
}

export function validateBridgePayload(p) {
  if (!p || p.v !== 1) return false;
  if (typeof p.method !== 'string' || p.method.length > 64) return false;
  if (typeof p.nonce !== 'string' || p.nonce.length > 64) return false;
  if (typeof p.ts !== 'number') return false;
  if (typeof p.sig !== 'string' || p.sig.length > 256) return false;

  const now = Date.now();
  if (Math.abs(now - p.ts) > BRIDGE_WINDOW_MS) return false;
  return true;
}

6.4 关键落地点(强烈建议)

  • Native 侧做最终校验:H5 校验只能作为“早失败”,不可当作安全边界
  • 强制 Origin/Host 校验:页面来源必须是白名单 + HTTPS
  • 能力分级授权:高危能力(支付/隐私/系统设置)必须二次确认或挑战
  • 失败默认拒绝:任何字段缺失/异常都拒绝并打点

参考:Android 官方对不安全 WebView Native Bridge 风险与缓解建议(如 addJavascriptInterface 相关)可作为审计依据。

7. WebView 安全配置要点(平台侧)

7.1 Android(概念性清单)

重点目标:关闭不必要能力 + 约束资源加载 + 禁用危险文件访问

  • 禁止明文流量(usesCleartextTraffic=false 或网络安全配置)
  • Mixed Content:优先 NEVER_ALLOW
  • Safe Browsing:保持开启并处理回调
  • File 访问:关闭 file:// 相关访问开关
  • JS 接口:避免对不受控页面暴露;不需要就移除
  • 调试:线上禁用 WebView 调试

7.2 iOS(概念性清单)

重点目标:ATS 强制 HTTPS + 导航白名单 + message handler 输入校验

  • ATS:避免全局放开;确有必要只对 Web 内容做最小化例外
  • WKNavigationDelegate:对所有跳转做 allowlist
  • WKContentRuleList:可用于阻断跟踪器/恶意资源
  • WKScriptMessageHandler:只注册必要通道,严格校验入参

8. 本地存储与敏感信息处理

8.1 原则

  • 不要把长期凭证放在 Web Storage(localStorage/IndexedDB 在多种场景下都更容易被脚本与调试工具读取)
  • Web 侧只持有短期令牌:必要时由 Native 侧持有长期密钥/凭证
  • 日志脱敏:禁止记录 Token/手机号/身份证等敏感明文

8.2 精简代码示例:WebCrypto 对业务缓存做封装(降低“误存明文”概率)

说明:这不是替代系统级安全存储(Keychain/Keystore),而是用于 H5 侧缓存不得不落地时,至少做到“默认不明文”。密钥应来自 Native 下发的短期会话密钥或用户派生密钥。

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

  const iv = crypto.getRandomValues(new Uint8Array(12));
  const data = new TextEncoder().encode(plaintext);
  const ct = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, data);

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

9. 发布与供应链安全(Hybrid 特别重要)

9.1 离线包/热更新的“签名与回滚”

  • 离线包必须签名:Native 校验签名通过才可加载
  • 资源完整性校验:hash 清单(manifest)+ 校验失败即回滚
  • 灰度与熔断:支持按版本/人群下线某个包或某个能力

9.2 第三方脚本治理

  • 尽量 自托管 第三方依赖,减少运行时外链
  • 需要外链时:强制 HTTPS + 版本锁定 +(可用时)SRI
  • 建立依赖审计:漏洞公告、版本基线、紧急替换策略

10. 监控、审计与应急响应

10.1 需要记录什么(不记录什么)

  • 记录:traceId、页面来源、method、参数摘要(长度/类型)、结果码、耗时
  • 不记录:Token、密码、验证码、隐私字段明文

10.2 事件响应流程(Mermaid)

flowchart LR
  A[发现异常: 监控/告警/反馈] --> B[初判与分级]
  B --> C{是否涉及敏感数据/资金?}
  C -- 是 --> D[紧急止血: 熔断能力/下线配置/回滚离线包]
  C -- 否 --> E[常规处理: 修复与灰度]
  D --> F[取证: 日志/调用链/包版本/环境信息]
  E --> F
  F --> G[修复: 规则/代码/配置]
  G --> H[验证: 回归/攻击复现]
  H --> I[复盘: 根因/改进项/基线升级]

11. 验收清单(可直接用于评审/测试)

11.1 加载与来源

  • 仅允许 https,禁止明文与未知 scheme
  • 域名/路径白名单生效,重定向不会跳出白名单
  • 离线包签名校验失败必回滚

11.2 Bridge

  • 每个方法有明确权限级别与页面来源限制
  • Native 侧对参数做严格校验(类型/长度/枚举)
  • nonce + timestamp 防重放,存在可观测 traceId

11.3 Web

  • CSP 已上线并可逐步收紧(至少限制 script-src)
  • Cookie 使用 SecureHttpOnly,SameSite 经过业务验证

11.4 存储与日志

  • Web Storage 不存长期凭证
  • 日志字段脱敏,敏感信息不落地

12. 进一步阅读(官方/权威)

13. 实战:构建完整的安全 JSBridge SDK

13.1 设计目标

从零构建一个生产级 JSBridge SDK,集成前面所有安全要素:

  • 来源校验:页面白名单 + HTTPS 强制
  • 消息签名:HMAC-SHA256 + nonce + timestamp
  • 能力授权:多级权限模型
  • 审计日志:traceId + 结构化日志
  • 降级策略:熔断开关 + 兜底响应

13.2 SDK 架构图

graph TB
  subgraph "H5 层"
    A[业务调用] --> B[BridgeSDK]
    B --> C{权限预检}
    C -- 通过 --> D[构建消息]
    C -- 拒绝 --> E[本地拒绝]
    D --> F[添加签名]
    F --> G[postMessage]
  end

  subgraph "Native 层"
    G --> H[消息接收]
    H --> I{来源校验}
    I -- 失败 --> J[拒绝+审计]
    I -- 通过 --> K{签名校验}
    K -- 失败 --> J
    K -- 通过 --> L{权限校验}
    L -- 失败 --> J
    L -- 通过 --> M[执行能力]
    M --> N[返回结果]
  end

  N --> O[H5 接收回调]
  J --> P[H5 接收错误]

13.3 完整代码实现

13.3.1 H5 侧:BridgeSDK 核心类
import crypto from 'crypto-js'; // 实际项目建议使用 Web Crypto API

class SecureBridgeSDK {
  constructor(config) {
    this.config = {
      allowedOrigins: config.allowedOrigins || [],
      sessionKey: config.sessionKey, // 由 Native 在页面初始化时注入
      timeout: config.timeout || 10000,
      enableAudit: config.enableAudit !== false,
    };

    this.callbackId = 0;
    this.callbacks = new Map();
    this.auditLogs = [];

    this._setupMessageListener();
  }

  /**
   * 调用 Native 能力
   * @param {string} method - 方法名
   * @param {object} params - 参数
   * @param {object} options - 选项(如权限级别)
   */
  async invoke(method, params = {}, options = {}) {
    const traceId = this._generateTraceId();
    const startTime = Date.now();

    try {
      // 1. 权限预检(客户端侧早失败)
      if (!this._checkPermission(method, options.requiredLevel)) {
        throw new Error('PERMISSION_DENIED');
      }

      // 2. 构建消息体
      const callbackId = `cb_${this.callbackId++}`;
      const nonce = this._generateNonce();
      const ts = Date.now();

      const payload = {
        v: 1,
        traceId,
        method,
        params,
        nonce,
        ts,
        callbackId,
      };

      // 3. 添加签名
      payload.sig = this._signPayload(payload);

      // 4. 发送消息并等待回调
      const result = await this._postMessage(payload, callbackId);

      // 5. 审计日志
      this._audit({
        traceId,
        method,
        success: true,
        duration: Date.now() - startTime,
      });

      return result;

    } catch (error) {
      this._audit({
        traceId,
        method,
        success: false,
        error: error.message,
        duration: Date.now() - startTime,
      });
      throw error;
    }
  }

  /**
   * 生成消息签名
   */
  _signPayload(payload) {
    const { v, traceId, method, params, nonce, ts } = payload;
    const message = `${v}|${traceId}|${method}|${JSON.stringify(params)}|${nonce}|${ts}`;

    return crypto.HmacSHA256(message, this.config.sessionKey).toString();
  }

  /**
   * 权限预检
   */
  _checkPermission(method, requiredLevel = 'normal') {
    // 实际项目应从 Native 获取当前上下文权限
    const permissions = {
      getUserInfo: 'normal',
      getLocation: 'sensitive',
      pay: 'critical',
      accessContacts: 'critical',
    };

    const levels = ['normal', 'sensitive', 'critical'];
    const methodLevel = permissions[method] || 'normal';

    return levels.indexOf(methodLevel) <= levels.indexOf(requiredLevel);
  }

  /**
   * 发送消息到 Native
   */
  _postMessage(payload, callbackId) {
    return new Promise((resolve, reject) => {
      const timer = setTimeout(() => {
        this.callbacks.delete(callbackId);
        reject(new Error('TIMEOUT'));
      }, this.config.timeout);

      this.callbacks.set(callbackId, { resolve, reject, timer });

      // 根据平台选择通信方式
      if (window.webkit?.messageHandlers?.bridge) {
        // iOS WKWebView
        window.webkit.messageHandlers.bridge.postMessage(payload);
      } else if (window.AndroidBridge?.postMessage) {
        // Android
        window.AndroidBridge.postMessage(JSON.stringify(payload));
      } else {
        // 降级方案:iframe scheme
        this._sendViaIframe(payload);
      }
    });
  }

  /**
   * 监听 Native 回调
   */
  _setupMessageListener() {
    window.__bridgeCallback__ = (response) => {
      const { callbackId, ok, data, code, traceId } = response;
      const callback = this.callbacks.get(callbackId);

      if (!callback) return;

      clearTimeout(callback.timer);
      this.callbacks.delete(callbackId);

      if (ok) {
        callback.resolve(data);
      } else {
        callback.reject(new Error(code || 'UNKNOWN_ERROR'));
      }
    };
  }

  /**
   * 审计日志
   */
  _audit(log) {
    if (!this.config.enableAudit) return;

    this.auditLogs.push({
      ...log,
      timestamp: Date.now(),
      url: window.location.href,
    });

    // 定期上报
    if (this.auditLogs.length >= 10) {
      this._flushAuditLogs();
    }
  }

  _flushAuditLogs() {
    if (this.auditLogs.length === 0) return;

    const logs = this.auditLogs.splice(0);

    // 上报到监控平台(去除敏感信息)
    fetch('https://monitor.example.com/audit', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        type: 'bridge_audit',
        logs: logs.map(l => ({
          traceId: l.traceId,
          method: l.method,
          success: l.success,
          duration: l.duration,
          timestamp: l.timestamp,
        })),
      }),
    }).catch(() => {
      // 静默失败,不影响主流程
    });
  }

  _generateTraceId() {
    return `${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
  }

  _generateNonce() {
    return Math.random().toString(36).substr(2, 16);
  }

  _sendViaIframe(payload) {
    const url = `jsbridge://invoke?payload=${encodeURIComponent(JSON.stringify(payload))}`;
    const iframe = document.createElement('iframe');
    iframe.style.display = 'none';
    iframe.src = url;
    document.body.appendChild(iframe);
    setTimeout(() => iframe.remove(), 100);
  }
}

export default SecureBridgeSDK;
13.3.2 Native 侧:消息校验伪代码(概念)
// 以下为概念性伪代码,展示 Native 侧应做的校验流程

class BridgeMessageValidator {
  validate(payload, context) {
    const checks = [
      () => this.checkOrigin(context.url),
      () => this.checkStructure(payload),
      () => this.checkTimestamp(payload.ts),
      () => this.checkNonce(payload.nonce),
      () => this.checkSignature(payload),
      () => this.checkPermission(payload.method, context.userLevel),
    ];

    for (const check of checks) {
      const result = check();
      if (!result.ok) {
        this.audit({
          traceId: payload.traceId,
          checkFailed: result.reason,
          url: context.url,
          method: payload.method,
        });
        return result;
      }
    }

    return { ok: true };
  }

  checkOrigin(url) {
    const allowedOrigins = [
      'https://m.example.com',
      'https://static.example.com',
    ];

    try {
      const u = new URL(url);
      if (u.protocol !== 'https:') {
        return { ok: false, reason: 'NON_HTTPS' };
      }

      const allowed = allowedOrigins.some(origin => url.startsWith(origin));
      return allowed ? { ok: true } : { ok: false, reason: 'ORIGIN_NOT_ALLOWED' };
    } catch {
      return { ok: false, reason: 'INVALID_URL' };
    }
  }

  checkSignature(payload) {
    const { v, traceId, method, params, nonce, ts, sig } = payload;
    const message = `${v}|${traceId}|${method}|${JSON.stringify(params)}|${nonce}|${ts}`;

    // 从安全存储中获取会话密钥
    const sessionKey = this.getSessionKey();
    const expectedSig = hmacSha256(message, sessionKey);

    const isValid = constantTimeCompare(sig, expectedSig);
    return isValid ? { ok: true } : { ok: false, reason: 'SIGNATURE_MISMATCH' };
  }

  checkTimestamp(ts) {
    const now = Date.now();
    const diff = Math.abs(now - ts);
    const MAX_WINDOW = 30000; // 30秒时间窗

    return diff <= MAX_WINDOW
      ? { ok: true }
      : { ok: false, reason: 'TIMESTAMP_OUT_OF_WINDOW' };
  }

  checkNonce(nonce) {
    // 检查 nonce 是否已被使用(防重放)
    if (this.nonceCache.has(nonce)) {
      return { ok: false, reason: 'NONCE_REUSED' };
    }

    this.nonceCache.set(nonce, true);
    setTimeout(() => this.nonceCache.delete(nonce), 60000); // 60秒后清理

    return { ok: true };
  }

  checkPermission(method, userLevel) {
    const permissions = {
      getUserInfo: 'user',
      getLocation: 'user',
      pay: 'verified',
      accessContacts: 'admin',
    };

    const requiredLevel = permissions[method] || 'user';
    const levels = { guest: 0, user: 1, verified: 2, admin: 3 };

    return levels[userLevel] >= levels[requiredLevel]
      ? { ok: true }
      : { ok: false, reason: 'INSUFFICIENT_PERMISSION' };
  }
}

13.4 使用示例

// 页面初始化
const bridge = new SecureBridgeSDK({
  sessionKey: window.__NATIVE_SESSION_KEY__, // Native 注入
  allowedOrigins: ['https://m.example.com'],
  timeout: 10000,
});

// 调用普通能力
async function getUserInfo() {
  try {
    const user = await bridge.invoke('getUserInfo', {}, { requiredLevel: 'normal' });
    console.log('用户信息:', user);
  } catch (error) {
    console.error('获取用户信息失败:', error.message);
  }
}

// 调用敏感能力
async function requestLocation() {
  try {
    const location = await bridge.invoke('getLocation', {
      accuracy: 'high',
    }, { requiredLevel: 'sensitive' });

    console.log('位置:', location);
  } catch (error) {
    if (error.message === 'PERMISSION_DENIED') {
      alert('需要位置权限才能继续');
    }
  }
}

// 调用支付能力(最高风险等级)
async function pay(orderInfo) {
  try {
    const result = await bridge.invoke('pay', {
      orderId: orderInfo.id,
      amount: orderInfo.amount,
      // 不传递敏感支付凭证,由 Native 侧管理
    }, { requiredLevel: 'critical' });

    return result;
  } catch (error) {
    console.error('支付失败:', error.message);
    throw error;
  }
}

14. 渗透测试与漏洞扫描

14.1 Hybrid 安全测试清单

以下清单可用于手动测试或集成到自动化安全流水线:

测试项测试方法预期结果
URL Scheme 劫持尝试通过外部 app 触发 scheme应拒绝或要求二次确认
HTTP 明文加载修改 URL 为 http://应被阻止或自动升级为 HTTPS
重定向跳出白名单302 跳转到恶意域名应拦截并审计
XSS 注入在输入框/URL 参数注入脚本应被 CSP 阻止或转义
Bridge 参数污染发送超长/畸形参数Native 应校验拒绝
重放攻击重复发送已抓包的 Bridge 消息nonce + timestamp 应拒绝
签名伪造修改消息后尝试调用签名校验应失败
未授权能力调用低权限页面调用高危能力应被权限系统拒绝
本地存储泄露读取 localStorage/IndexedDB不应有长期凭证
调试接口暴露检查线上包是否开启调试线上应禁用

14.2 自动化扫描脚本示例

/**
 * 安全扫描脚本:检测常见 Hybrid 安全问题
 * 运行环境:Node.js + Puppeteer
 */

import puppeteer from 'puppeteer';

class HybridSecurityScanner {
  constructor(targetUrl) {
    this.targetUrl = targetUrl;
    this.findings = [];
  }

  async scan() {
    const browser = await puppeteer.launch({ headless: true });
    const page = await browser.newPage();

    try {
      await page.goto(this.targetUrl, { waitUntil: 'networkidle0' });

      await this.checkCSP(page);
      await this.checkMixedContent(page);
      await this.checkLocalStorage(page);
      await this.checkBridgeExposure(page);
      await this.testXSS(page);

      console.log('扫描完成,发现问题:');
      this.findings.forEach((f, i) => {
        console.log(`${i + 1}. [${f.severity}] ${f.title}: ${f.description}`);
      });

    } finally {
      await browser.close();
    }

    return this.findings;
  }

  async checkCSP(page) {
    const csp = await page.evaluate(() => {
      const meta = document.querySelector('meta[http-equiv="Content-Security-Policy"]');
      return meta ? meta.content : null;
    });

    if (!csp) {
      this.findings.push({
        severity: 'HIGH',
        title: '缺少 CSP',
        description: '页面未设置 Content-Security-Policy',
      });
    } else if (csp.includes("'unsafe-inline'")) {
      this.findings.push({
        severity: 'MEDIUM',
        title: 'CSP 过于宽松',
        description: "CSP 允许 'unsafe-inline',可能存在 XSS 风险",
      });
    }
  }

  async checkMixedContent(page) {
    const requests = [];

    page.on('request', req => {
      if (req.url().startsWith('http://')) {
        requests.push(req.url());
      }
    });

    await page.reload({ waitUntil: 'networkidle0' });

    if (requests.length > 0) {
      this.findings.push({
        severity: 'HIGH',
        title: '混合内容',
        description: `检测到 ${requests.length} 个 HTTP 资源: ${requests.slice(0, 3).join(', ')}`,
      });
    }
  }

  async checkLocalStorage(page) {
    const sensitiveKeys = await page.evaluate(() => {
      const keys = Object.keys(localStorage);
      const sensitive = ['token', 'password', 'secret', 'key', 'auth'];

      return keys.filter(k =>
        sensitive.some(s => k.toLowerCase().includes(s))
      );
    });

    if (sensitiveKeys.length > 0) {
      this.findings.push({
        severity: 'CRITICAL',
        title: 'localStorage 存储敏感信息',
        description: `发现敏感键: ${sensitiveKeys.join(', ')}`,
      });
    }
  }

  async checkBridgeExposure(page) {
    const exposedMethods = await page.evaluate(() => {
      const exposed = [];

      if (window.webkit?.messageHandlers) {
        exposed.push('iOS WKWebView messageHandlers');
      }

      if (window.AndroidBridge) {
        const methods = Object.getOwnPropertyNames(window.AndroidBridge);
        exposed.push(`Android Bridge: ${methods.join(', ')}`);
      }

      return exposed;
    });

    if (exposedMethods.length > 0) {
      this.findings.push({
        severity: 'INFO',
        title: 'JSBridge 接口暴露',
        description: exposedMethods.join('; '),
      });
    }
  }

  async testXSS(page) {
    const xssPayloads = [
      '<script>alert(1)</script>',
      '<img src=x onerror=alert(1)>',
      'javascript:alert(1)',
    ];

    for (const payload of xssPayloads) {
      try {
        await page.evaluate((p) => {
          const input = document.querySelector('input[type="text"]');
          if (input) {
            input.value = p;
            input.dispatchEvent(new Event('input', { bubbles: true }));
          }
        }, payload);

        const alertDetected = await page.evaluate(() => {
          return document.body.innerHTML.includes('<script>');
        });

        if (alertDetected) {
          this.findings.push({
            severity: 'CRITICAL',
            title: 'XSS 漏洞',
            description: `Payload "${payload}" 未被正确转义`,
          });
          break;
        }
      } catch (e) {
        // 继续下一个 payload
      }
    }
  }
}

// 使用示例
const scanner = new HybridSecurityScanner('https://m.example.com/hybrid/');
scanner.scan().then(findings => {
  process.exit(findings.some(f => f.severity === 'CRITICAL') ? 1 : 0);
});

15. 真实攻击案例与防护复盘

15.1 案例 1:XSS 导致 Bridge 越权调用

攻击场景

某电商 Hybrid App 的商品详情页存在反射型 XSS 漏洞,攻击者通过构造恶意 URL 注入脚本:

https://m.shop.com/item?id=123&comment=<script>window.bridge.pay({amount:9999})</script>

攻击流程

sequenceDiagram
  participant Victim as 受害用户
  participant Attacker as 攻击者
  participant App as Hybrid App
  participant Server as 后端服务

  Attacker->>Victim: 发送钓鱼链接(含 XSS)
  Victim->>App: 点击链接打开页面
  App->>App: 未转义注入脚本执行
  App->>App: 调用 bridge.pay()
  App->>Server: 发起支付请求
  Server->>Victim: 扣款成功

根因分析

  1. Web 层:URL 参数未经转义直接渲染到 DOM
  2. CSP 缺失:未设置 script-src 限制
  3. Bridge 层:未校验调用来源,任意脚本可调用

修复方案

// 1. Web 层:严格转义
function sanitizeInput(input) {
  const div = document.createElement('div');
  div.textContent = input;
  return div.innerHTML;
}

const comment = new URLSearchParams(location.search).get('comment');
document.getElementById('comment').innerHTML = sanitizeInput(comment);

// 2. 添加 CSP
<meta http-equiv="Content-Security-Policy"
      content="script-src 'nonce-{RANDOM}' 'strict-dynamic'; object-src 'none'; base-uri 'none';">

// 3. Bridge 层:调用栈校验
class SecureBridge {
  pay(params) {
    // 检查调用来源
    const stack = new Error().stack;
    const callerAllowed = this.checkCallerInWhitelist(stack);

    if (!callerAllowed) {
      this.audit({ event: 'UNAUTHORIZED_PAY_CALL', stack });
      throw new Error('PERMISSION_DENIED');
    }

    // 二次确认
    return this.nativeConfirm('确认支付?').then(confirmed => {
      if (confirmed) {
        return this.nativePay(params);
      }
    });
  }
}

15.2 案例 2:中间人攻击篡改离线包

攻击场景

用户在公共 WiFi 下使用 App,攻击者通过 ARP 欺骗进行中间人攻击,篡改离线包下载响应:

sequenceDiagram
  participant App as Hybrid App
  participant Attacker as 中间人
  participant CDN as 离线包 CDN

  App->>CDN: GET /packages/home_v1.2.3.zip
  CDN->>Attacker: 返回正常离线包
  Attacker->>Attacker: 注入恶意脚本
  Attacker->>App: 返回篡改后的包
  App->>App: 未校验完整性,加载恶意包
  App->>Attacker: 泄露用户数据

根因分析

  1. 离线包未使用 HTTPS
  2. 未校验包签名/hash
  3. 未实现证书 Pinning

修复方案

// 1. 离线包管理器:强制签名校验
class OfflinePackageManager {
  async download(packageUrl, expectedHash) {
    // 强制 HTTPS
    if (!packageUrl.startsWith('https://')) {
      throw new Error('INSECURE_PROTOCOL');
    }

    const response = await fetch(packageUrl);
    const buffer = await response.arrayBuffer();

    // 校验 hash
    const actualHash = await this.computeHash(buffer);
    if (actualHash !== expectedHash) {
      this.audit({
        event: 'PACKAGE_HASH_MISMATCH',
        url: packageUrl,
        expected: expectedHash,
        actual: actualHash,
      });
      throw new Error('INTEGRITY_CHECK_FAILED');
    }

    // 校验签名(假设签名在 HTTP 响应头中)
    const signature = response.headers.get('X-Package-Signature');
    const isValid = await this.verifySignature(buffer, signature);

    if (!isValid) {
      throw new Error('SIGNATURE_INVALID');
    }

    return buffer;
  }

  async computeHash(buffer) {
    const hashBuffer = await crypto.subtle.digest('SHA-256', buffer);
    return Array.from(new Uint8Array(hashBuffer))
      .map(b => b.toString(16).padStart(2, '0'))
      .join('');
  }

  async verifySignature(data, signature) {
    // 使用内置的公钥验证签名
    const publicKey = await this.getPublicKey();
    const isValid = await crypto.subtle.verify(
      { name: 'RSASSA-PKCS1-v1_5' },
      publicKey,
      this.base64Decode(signature),
      data
    );
    return isValid;
  }
}

// 2. Native 侧:证书 Pinning(概念代码)
// iOS (Swift)
let session = URLSession(
  configuration: .default,
  delegate: CertificatePinningDelegate(),
  delegateQueue: nil
)

class CertificatePinningDelegate: NSObject, URLSessionDelegate {
  func urlSession(
    _ session: URLSession,
    didReceive challenge: URLAuthenticationChallenge,
    completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void
  ) {
    guard let serverTrust = challenge.protectionSpace.serverTrust else {
      completionHandler(.cancelAuthenticationChallenge, nil)
      return
    }

    let certificate = SecTrustGetCertificateAtIndex(serverTrust, 0)
    let serverCertData = SecCertificateCopyData(certificate) as Data

    // 与预置的证书 hash 对比
    let pinnedHashes = ["sha256/AAAAAAA...", "sha256/BBBBBBB..."]
    let serverHash = sha256(serverCertData)

    if pinnedHashes.contains(serverHash) {
      completionHandler(.useCredential, URLCredential(trust: serverTrust))
    } else {
      completionHandler(.cancelAuthenticationChallenge, nil)
    }
  }
}

15.3 案例 3:调试接口泄露导致数据窃取

攻击场景

某社交 App 在线上包中遗留了调试开关,攻击者通过反编译发现可通过特定 URL 参数开启调试模式,从而窃取用户聊天记录:

https://m.social.com/chat?__debug__=1&__export_logs__=1

根因分析

  1. 构建流程未区分 debug/release
  2. 调试代码未被 tree-shaking 移除
  3. 缺少运行时环境检测

修复方案

// 1. 构建配置:环境变量严格区分
// vite.config.js
export default defineConfig({
  define: {
    __DEV__: JSON.stringify(process.env.NODE_ENV === 'development'),
  },
  build: {
    minify: 'terser',
    terserOptions: {
      compress: {
        drop_console: true,
        drop_debugger: true,
        pure_funcs: ['console.log', 'console.debug'],
      },
    },
  },
});

// 2. 代码中使用常量判断(会被 tree-shaking)
if (__DEV__) {
  // 此代码在生产构建中会被完全移除
  window.__enableDebug__ = () => {
    localStorage.setItem('debug', '1');
  };
}

// 3. Native 侧:运行时检测
class DebugGuard {
  static checkEnvironment() {
    const isDebugBuild = /* Native 侧检测是否为 debug 构建 */;
    const isJailbroken = /* 检测越狱/Root */;
    const isDebugging = /* 检测是否正在被调试 */;

    if (!isDebugBuild && (isJailbroken || isDebugging)) {
      // 记录异常并退出或降级
      this.audit({ event: 'ABNORMAL_ENVIRONMENT', isJailbroken, isDebugging });

      // 选择:退出应用 或 禁用敏感功能
      this.disableSensitiveFeatures();
    }
  }

  static disableSensitiveFeatures() {
    // 禁用支付、敏感数据访问等
    window.bridge.disableCapabilities(['pay', 'accessContacts', 'getMessages']);
  }
}

16. 安全监控与实时防护

16.1 监控指标体系

指标类别具体指标阈值示例响应动作
加载异常非白名单 URL 加载> 0立即告警 + 阻断
Bridge 异常签名校验失败率> 1%告警 + 灰度回滚
XSS 尝试CSP 违规上报> 10/分钟告警 + IP 封禁
性能异常离线包下载失败率> 5%降级到在线加载
设备异常越狱/Root 设备占比> 10%限制敏感功能

16.2 实时监控 SDK 实现

/**
 * 安全监控 SDK
 * 采集安全事件并实时上报
 */
class SecurityMonitor {
  constructor(config) {
    this.config = {
      reportUrl: config.reportUrl,
      samplingRate: config.samplingRate || 1.0, // 采样率
      batchSize: config.batchSize || 10,
      flushInterval: config.flushInterval || 5000,
    };

    this.events = [];
    this.sessionId = this._generateSessionId();

    this._setupInterceptors();
    this._startFlushTimer();
  }

  /**
   * 记录安全事件
   */
  record(event) {
    if (Math.random() > this.config.samplingRate) {
      return; // 采样丢弃
    }

    this.events.push({
      ...event,
      sessionId: this.sessionId,
      timestamp: Date.now(),
      url: window.location.href,
      ua: navigator.userAgent,
    });

    if (this.events.length >= this.config.batchSize) {
      this.flush();
    }
  }

  /**
   * 设置拦截器
   */
  _setupInterceptors() {
    // 拦截 CSP 违规
    document.addEventListener('securitypolicyviolation', (e) => {
      this.record({
        type: 'csp_violation',
        severity: 'high',
        directive: e.violatedDirective,
        blockedURI: e.blockedURI,
        sourceFile: e.sourceFile,
        lineNumber: e.lineNumber,
      });
    });

    // 拦截 Bridge 调用
    if (window.bridge) {
      const originalInvoke = window.bridge.invoke;
      window.bridge.invoke = async (method, params, options) => {
        const startTime = Date.now();

        try {
          const result = await originalInvoke.call(window.bridge, method, params, options);

          this.record({
            type: 'bridge_call',
            severity: 'info',
            method,
            success: true,
            duration: Date.now() - startTime,
          });

          return result;
        } catch (error) {
          this.record({
            type: 'bridge_call',
            severity: 'error',
            method,
            success: false,
            error: error.message,
            duration: Date.now() - startTime,
          });

          throw error;
        }
      };
    }

    // 拦截网络请求
    const originalFetch = window.fetch;
    window.fetch = async (...args) => {
      const [url] = args;

      if (typeof url === 'string' && !url.startsWith('https://')) {
        this.record({
          type: 'insecure_request',
          severity: 'high',
          url,
        });
      }

      return originalFetch.apply(window, args);
    };

    // 监听页面跳转
    window.addEventListener('beforeunload', () => {
      this.flush(true); // 同步刷新
    });
  }

  /**
   * 上报事件
   */
  flush(sync = false) {
    if (this.events.length === 0) return;

    const payload = {
      events: this.events.splice(0),
      deviceId: this._getDeviceId(),
      appVersion: this._getAppVersion(),
    };

    if (sync) {
      // 同步上报(页面卸载时)
      navigator.sendBeacon(this.config.reportUrl, JSON.stringify(payload));
    } else {
      // 异步上报
      fetch(this.config.reportUrl, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(payload),
        keepalive: true,
      }).catch(() => {
        // 静默失败
      });
    }
  }

  _startFlushTimer() {
    setInterval(() => this.flush(), this.config.flushInterval);
  }

  _generateSessionId() {
    return `${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
  }

  _getDeviceId() {
    return window.bridge?.getDeviceId?.() || 'unknown';
  }

  _getAppVersion() {
    return window.bridge?.getAppVersion?.() || 'unknown';
  }
}

// 初始化监控
const monitor = new SecurityMonitor({
  reportUrl: 'https://monitor.example.com/security/report',
  samplingRate: 0.1, // 10% 采样
  batchSize: 20,
});

// 导出全局实例
window.__securityMonitor__ = monitor;

16.3 服务端实时分析与熔断

/**
 * 服务端安全分析引擎(概念性代码)
 * 实时分析上报事件并触发防护动作
 */
class SecurityAnalysisEngine {
  constructor() {
    this.rules = this._loadRules();
    this.stats = new Map(); // 统计窗口
  }

  /**
   * 分析事件批次
   */
  async analyze(events) {
    for (const event of events) {
      await this._updateStats(event);
      await this._checkRules(event);
    }
  }

  /**
   * 更新统计信息
   */
  async _updateStats(event) {
    const key = `${event.type}_${event.severity}`;
    const window = this.stats.get(key) || { count: 0, lastReset: Date.now() };

    // 滑动窗口:每分钟重置
    if (Date.now() - window.lastReset > 60000) {
      window.count = 0;
      window.lastReset = Date.now();
    }

    window.count++;
    this.stats.set(key, window);
  }

  /**
   * 检查规则并触发动作
   */
  async _checkRules(event) {
    for (const rule of this.rules) {
      if (rule.match(event)) {
        const action = rule.getAction(event, this.stats);
        await this._executeAction(action, event);
      }
    }
  }

  /**
   * 执行防护动作
   */
  async _executeAction(action, event) {
    switch (action.type) {
      case 'ALERT':
        await this._sendAlert(action.message, event);
        break;

      case 'BLOCK_IP':
        await this._blockIP(event.ip);
        break;

      case 'CIRCUIT_BREAK':
        await this._enableCircuitBreaker(action.capability);
        break;

      case 'ROLLBACK':
        await this._rollbackPackage(action.packageId);
        break;
    }
  }

  /**
   * 加载规则
   */
  _loadRules() {
    return [
      {
        name: '高频 CSP 违规',
        match: (event) => event.type === 'csp_violation',
        getAction: (event, stats) => {
          const count = stats.get('csp_violation_high')?.count || 0;
          if (count > 100) {
            return { type: 'BLOCK_IP', ip: event.ip };
          }
          return { type: 'ALERT', message: `CSP 违规激增: ${count}/分钟` };
        },
      },
      {
        name: 'Bridge 签名失败',
        match: (event) => event.type === 'bridge_call' && event.error === 'SIGNATURE_MISMATCH',
        getAction: (event) => ({
          type: 'CIRCUIT_BREAK',
          capability: event.method,
        }),
      },
      {
        name: '离线包完整性失败',
        match: (event) => event.type === 'package_integrity_failed',
        getAction: (event) => ({
          type: 'ROLLBACK',
          packageId: event.packageId,
        }),
      },
    ];
  }

  async _sendAlert(message, event) {
    console.log(`[ALERT] ${message}`, event);
    // 实际项目应发送到告警系统(钉钉/PagerDuty 等)
  }

  async _blockIP(ip) {
    console.log(`[BLOCK] IP ${ip} blocked`);
    // 调用 WAF/CDN API 封禁 IP
  }

  async _enableCircuitBreaker(capability) {
    console.log(`[CIRCUIT_BREAK] 熔断能力: ${capability}`);
    // 通过配置中心下发熔断开关
  }

  async _rollbackPackage(packageId) {
    console.log(`[ROLLBACK] 回滚离线包: ${packageId}`);
    // 更新配置,让客户端回退到上一版本
  }
}

17. 合规性与隐私保护

17.1 GDPR / 个人信息保护法 合规要点

要求Hybrid 落地方案
数据最小化Bridge 接口只暴露必需能力;请求只携带必需参数
明示同意敏感能力(定位/通讯录)首次调用前弹窗确认
数据加密传输:HTTPS + Pinning; 存储:WebCrypto/Keychain
可删除权提供"清除缓存"功能,删除 localStorage/IndexedDB
数据导出提供审计日志导出接口(需脱敏)
跨境传输敏感数据不出境;或经用户同意+安全评估

17.2 隐私合规代码示例

/**
 * 隐私合规管理器
 * 管理用户授权与数据收集
 */
class PrivacyComplianceManager {
  constructor() {
    this.consents = this._loadConsents();
  }

  /**
   * 请求敏感能力前检查授权
   */
  async requestCapability(capability, purpose) {
    const consentKey = `consent_${capability}`;

    // 检查是否已授权
    if (this.consents[consentKey]) {
      return true;
    }

    // 弹窗请求授权
    const granted = await this._showConsentDialog(capability, purpose);

    if (granted) {
      this.consents[consentKey] = {
        granted: true,
        timestamp: Date.now(),
        purpose,
      };
      this._saveConsents();
    }

    // 记录授权结果
    this._auditConsent(capability, granted);

    return granted;
  }

  /**
   * 展示授权弹窗
   */
  async _showConsentDialog(capability, purpose) {
    const messages = {
      location: '我们需要访问您的位置信息以提供附近商家服务',
      contacts: '我们需要访问您的通讯录以便您邀请好友',
      camera: '我们需要访问您的相机以扫描二维码',
    };

    return new Promise((resolve) => {
      const dialog = document.createElement('div');
      dialog.innerHTML = `
        <div style="position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);z-index:9999;">
          <div style="background:white;margin:20% auto;padding:20px;width:80%;max-width:400px;border-radius:8px;">
            <h3>授权请求</h3>
            <p>${messages[capability] || purpose}</p>
            <div style="margin-top:20px;text-align:right;">
              <button id="deny" style="margin-right:10px;">拒绝</button>
              <button id="allow">允许</button>
            </div>
          </div>
        </div>
      `;

      document.body.appendChild(dialog);

      dialog.querySelector('#allow').onclick = () => {
        dialog.remove();
        resolve(true);
      };

      dialog.querySelector('#deny').onclick = () => {
        dialog.remove();
        resolve(false);
      };
    });
  }

  /**
   * 撤销授权
   */
  revokeConsent(capability) {
    const consentKey = `consent_${capability}`;
    delete this.consents[consentKey];
    this._saveConsents();
    this._auditConsent(capability, false, 'revoked');
  }

  /**
   * 清除所有数据(响应"删除我的数据"请求)
   */
  async deleteAllData() {
    // 清除 Web 存储
    localStorage.clear();
    sessionStorage.clear();

    // 清除 IndexedDB
    const databases = await indexedDB.databases();
    for (const db of databases) {
      indexedDB.deleteDatabase(db.name);
    }

    // 清除 Cookie
    document.cookie.split(';').forEach((c) => {
      document.cookie = c.replace(/^ +/, '').replace(/=.*/, `=;expires=${new Date().toUTCString()};path=/`);
    });

    // 通知 Native 清除本地数据
    await window.bridge?.invoke('clearAllData');

    console.log('所有本地数据已清除');
  }

  /**
   * 导出用户数据
   */
  async exportUserData() {
    const data = {
      consents: this.consents,
      localStorage: { ...localStorage },
      sessionStorage: { ...sessionStorage },
      cookies: document.cookie,
    };

    // 下载为 JSON 文件
    const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = `user_data_${Date.now()}.json`;
    a.click();
    URL.revokeObjectURL(url);
  }

  _loadConsents() {
    try {
      return JSON.parse(localStorage.getItem('privacy_consents') || '{}');
    } catch {
      return {};
    }
  }

  _saveConsents() {
    localStorage.setItem('privacy_consents', JSON.stringify(this.consents));
  }

  _auditConsent(capability, granted, action = 'request') {
    fetch('https://monitor.example.com/privacy/audit', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        capability,
        granted,
        action,
        timestamp: Date.now(),
      }),
    }).catch(() => {});
  }
}

// 使用示例
const privacy = new PrivacyComplianceManager();

async function getLocation() {
  const allowed = await privacy.requestCapability('location', '附近商家推荐');

  if (allowed) {
    const location = await window.bridge.invoke('getLocation');
    return location;
  } else {
    throw new Error('USER_DENIED_LOCATION');
  }
}

18. DevSecOps:安全自动化集成

18.1 CI/CD 安全检查流程

flowchart LR
  A[代码提交] --> B[静态扫描]
  B --> C{发现漏洞?}
  C -- 高危 --> D[阻止合并]
  C -- 中低危 --> E[警告+继续]
  E --> F[依赖审计]
  F --> G{存在已知漏洞?}
  G -- 是 --> H[自动升级/告警]
  G -- 否 --> I[构建]
  I --> J[动态扫描]
  J --> K[部署到灰度]
  K --> L[自动化渗透测试]
  L --> M{测试通过?}
  M -- 否 --> N[回滚+告警]
  M -- 是 --> O[全量发布]

18.2 自动化安全检查脚本

/**
 * CI/CD 安全检查脚本
 * package.json: "scripts": { "security-check": "node security-check.js" }
 */

import { execSync } from 'child_process';
import fs from 'fs';

class SecurityChecker {
  constructor() {
    this.findings = [];
    this.exitCode = 0;
  }

  async runAllChecks() {
    console.log('=== 开始安全检查 ===\n');

    await this.checkDependencies();
    await this.checkSecrets();
    await this.checkCSP();
    await this.checkBridgeConfig();

    this.printReport();
    process.exit(this.exitCode);
  }

  /**
   * 检查依赖漏洞
   */
  async checkDependencies() {
    console.log('[1/4] 检查依赖漏洞...');

    try {
      execSync('npm audit --json', { stdio: 'pipe' });
      console.log('✓ 未发现已知漏洞\n');
    } catch (error) {
      const audit = JSON.parse(error.stdout.toString());
      const vulnerabilities = audit.metadata.vulnerabilities;

      if (vulnerabilities.critical > 0 || vulnerabilities.high > 0) {
        this.findings.push({
          severity: 'CRITICAL',
          category: '依赖漏洞',
          message: `发现 ${vulnerabilities.critical} 个严重漏洞, ${vulnerabilities.high} 个高危漏洞`,
        });
        this.exitCode = 1;
      }

      console.log(`✗ ${vulnerabilities.total} 个漏洞\n`);
    }
  }

  /**
   * 检查硬编码密钥
   */
  async checkSecrets() {
    console.log('[2/4] 检查硬编码密钥...');

    const patterns = [
      /(['"])?(api[_-]?key|secret|token|password)(['"])?\s*[:=]\s*['"][^'"]{8,}['"]/gi,
      /-----BEGIN (RSA |)PRIVATE KEY-----/,
      /[a-zA-Z0-9]{32,}/g, // 长随机字符串
    ];

    const files = execSync('git ls-files "*.js" "*.ts"', { encoding: 'utf-8' })
      .split('\n')
      .filter(Boolean);

    let found = false;

    for (const file of files) {
      const content = fs.readFileSync(file, 'utf-8');

      for (const pattern of patterns) {
        if (pattern.test(content)) {
          this.findings.push({
            severity: 'HIGH',
            category: '硬编码密钥',
            message: `${file} 可能包含硬编码密钥`,
          });
          found = true;
          this.exitCode = 1;
        }
      }
    }

    console.log(found ? '✗ 发现疑似密钥\n' : '✓ 未发现硬编码密钥\n');
  }

  /**
   * 检查 CSP 配置
   */
  async checkCSP() {
    console.log('[3/4] 检查 CSP 配置...');

    const htmlFiles = execSync('git ls-files "*.html"', { encoding: 'utf-8' })
      .split('\n')
      .filter(Boolean);

    let hasCSP = false;

    for (const file of htmlFiles) {
      const content = fs.readFileSync(file, 'utf-8');

      if (content.includes('Content-Security-Policy')) {
        hasCSP = true;

        if (content.includes("'unsafe-inline'") || content.includes("'unsafe-eval'")) {
          this.findings.push({
            severity: 'MEDIUM',
            category: 'CSP 配置',
            message: `${file} 的 CSP 包含 unsafe-inline 或 unsafe-eval`,
          });
        }
      }
    }

    if (!hasCSP && htmlFiles.length > 0) {
      this.findings.push({
        severity: 'HIGH',
        category: 'CSP 配置',
        message: '未找到 CSP 配置',
      });
      this.exitCode = 1;
    }

    console.log(hasCSP ? '✓ CSP 已配置\n' : '✗ 缺少 CSP\n');
  }

  /**
   * 检查 Bridge 配置
   */
  async checkBridgeConfig() {
    console.log('[4/4] 检查 Bridge 安全配置...');

    const configFiles = ['bridge.config.js', 'src/bridge/config.ts'];
    let foundConfig = false;

    for (const file of configFiles) {
      if (fs.existsSync(file)) {
        const content = fs.readFileSync(file, 'utf-8');
        foundConfig = true;

        // 检查是否配置了白名单
        if (!content.includes('allowedOrigins') && !content.includes('whitelist')) {
          this.findings.push({
            severity: 'CRITICAL',
            category: 'Bridge 配置',
            message: `${file} 未配置 allowedOrigins 白名单`,
          });
          this.exitCode = 1;
        }

        // 检查是否启用签名
        if (!content.includes('signature') && !content.includes('sign')) {
          this.findings.push({
            severity: 'HIGH',
            category: 'Bridge 配置',
            message: `${file} 未启用消息签名`,
          });
        }
      }
    }

    if (!foundConfig) {
      this.findings.push({
        severity: 'MEDIUM',
        category: 'Bridge 配置',
        message: '未找到 Bridge 配置文件',
      });
    }

    console.log('✓ Bridge 配置检查完成\n');
  }

  printReport() {
    console.log('=== 检查报告 ===\n');

    if (this.findings.length === 0) {
      console.log('✓ 所有检查通过!\n');
      return;
    }

    const grouped = this.findings.reduce((acc, f) => {
      acc[f.severity] = acc[f.severity] || [];
      acc[f.severity].push(f);
      return acc;
    }, {});

    for (const [severity, items] of Object.entries(grouped)) {
      console.log(`${severity}:`);
      items.forEach(item => {
        console.log(`  - [${item.category}] ${item.message}`);
      });
      console.log('');
    }

    console.log(`总计: ${this.findings.length} 个问题`);
    console.log(this.exitCode === 0 ? '\n✓ 可以继续部署' : '\n✗ 阻止部署\n');
  }
}

const checker = new SecurityChecker();
checker.runAllChecks();

19. 性能与安全的平衡

19.1 常见性能-安全权衡

场景安全措施性能影响优化方案
Bridge 调用签名校验+5-10ms使用对称加密(HMAC)而非非对称;缓存会话密钥
离线包加载完整性校验+50-100ms增量校验;Worker 异步校验
CSPstrict-dynamic阻止内联脚本使用 nonce;构建时预处理
HTTPSTLS 握手+100-300msHTTP/2;会话复用;证书 OCSP Stapling
证书 Pinning每次请求校验+10-20ms缓存校验结果;只 pin 根证书

19.2 性能优化代码示例

/**
 * Bridge 调用性能优化
 * 通过批处理和缓存减少签名开销
 */
class OptimizedBridge {
  constructor(sessionKey) {
    this.sessionKey = sessionKey;
    this.signatureCache = new Map();
    this.batchQueue = [];
    this.batchTimer = null;
  }

  /**
   * 支持批量调用
   */
  invoke(method, params) {
    return new Promise((resolve, reject) => {
      this.batchQueue.push({ method, params, resolve, reject });

      if (!this.batchTimer) {
        this.batchTimer = setTimeout(() => this._flushBatch(), 10);
      }
    });
  }

  /**
   * 批量发送(减少签名次数)
   */
  _flushBatch() {
    clearTimeout(this.batchTimer);
    this.batchTimer = null;

    const batch = this.batchQueue.splice(0);
    if (batch.length === 0) return;

    // 单次签名覆盖整个批次
    const payload = {
      v: 1,
      batch: batch.map(b => ({ method: b.method, params: b.params })),
      nonce: this._generateNonce(),
      ts: Date.now(),
    };

    payload.sig = this._signPayload(payload);

    this._postMessage(payload).then(results => {
      results.forEach((result, i) => {
        if (result.ok) {
          batch[i].resolve(result.data);
        } else {
          batch[i].reject(new Error(result.code));
        }
      });
    });
  }

  /**
   * 签名缓存(相同参数复用签名)
   */
  _signPayload(payload) {
    const key = JSON.stringify(payload);

    if (this.signatureCache.has(key)) {
      return this.signatureCache.get(key);
    }

    const signature = crypto.HmacSHA256(key, this.sessionKey).toString();
    this.signatureCache.set(key, signature);

    // 限制缓存大小
    if (this.signatureCache.size > 100) {
      const firstKey = this.signatureCache.keys().next().value;
      this.signatureCache.delete(firstKey);
    }

    return signature;
  }

  /**
   * Worker 异步签名(不阻塞主线程)
   */
  async _signInWorker(payload) {
    const worker = new Worker('/sign-worker.js');

    return new Promise((resolve) => {
      worker.onmessage = (e) => {
        resolve(e.data.signature);
        worker.terminate();
      };

      worker.postMessage({ payload, sessionKey: this.sessionKey });
    });
  }
}

20. 总结与持续改进

20.1 安全成熟度模型

timeline
  title Hybrid 安全成熟度演进

  section Level 1: 基础防护
    HTTPS强制 : CSP配置 : 基础Bridge校验

  section Level 2: 纵深防御
    来源白名单 : 消息签名 : 离线包签名 : 日志审计

  section Level 3: 主动监控
    实时监控 : 异常检测 : 自动熔断 : 灰度回滚

  section Level 4: 安全左移
    安全扫描集成CI : 自动化渗透测试 : 依赖审计 : 供应链管理

  section Level 5: 持续优化
    威胁建模 : 红蓝对抗 : 安全文化 : 合规认证

20.2 核心要点回顾

  1. 最小暴露原则

    • 只暴露必需的 Bridge 接口
    • 能力分级授权(normal / sensitive / critical)
    • 默认拒绝策略
  2. 分层防御体系

    • Web 层:CSP + SameSite + 输入转义
    • Bridge 层:来源校验 + 签名 + 时间窗 + nonce
    • 网络层:HTTPS + Certificate Pinning
    • 发布层:离线包签名 + 完整性校验 + 灰度熔断
  3. 可验证性

    • 每个安全规则都有对应的自动化测试
    • 每次敏感调用都有审计日志(脱敏)
    • 每个异常都有 traceId 可回溯
  4. 持续改进

    • 定期进行渗透测试和代码审计
    • 建立漏洞响应流程(SLA < 24h for critical)
    • 安全培训与文化建设

20.3 下一步行动清单

  • 完成本文所有验收清单(第 11 章)
  • 集成自动化安全扫描到 CI/CD(第 18 章)
  • 部署实时监控 SDK(第 16 章)
  • 制定安全应急响应预案(第 10 章)
  • 每季度进行一次红队渗透测试
  • 建立安全基线并纳入技术规范

20.4 延伸阅读

官方标准与规范

技术深度文章

工具与框架

  • MobSF - 移动安全自动化扫描框架
  • OWASP ZAP - Web 应用安全扫描工具
  • Burp Suite - 渗透测试瑞士军刀