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-src与connect-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:对所有跳转做 allowlistWKContentRuleList:可用于阻断跟踪器/恶意资源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 使用
Secure与HttpOnly,SameSite 经过业务验证
11.4 存储与日志
- Web Storage 不存长期凭证
- 日志字段脱敏,敏感信息不落地
12. 进一步阅读(官方/权威)
- Android: Insecure WebView native bridges(风险与缓解)
- Android: Cross-app scripting(跨应用脚本风险)
- MDN: Content Security Policy(CSP 指令与示例)
- MDN: CSP
frame-ancestors(防点击劫持) - OWASP MAS(移动安全:MASVS / MASTG)
- Mermaid: Flowchart 语法
- Mermaid: Sequence Diagram 语法
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: 扣款成功
根因分析:
- Web 层:URL 参数未经转义直接渲染到 DOM
- CSP 缺失:未设置
script-src限制 - 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: 泄露用户数据
根因分析:
- 离线包未使用 HTTPS
- 未校验包签名/hash
- 未实现证书 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
根因分析:
- 构建流程未区分 debug/release
- 调试代码未被 tree-shaking 移除
- 缺少运行时环境检测
修复方案:
// 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 异步校验 |
| CSP | strict-dynamic | 阻止内联脚本 | 使用 nonce;构建时预处理 |
| HTTPS | TLS 握手 | +100-300ms | HTTP/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 核心要点回顾
-
最小暴露原则
- 只暴露必需的 Bridge 接口
- 能力分级授权(normal / sensitive / critical)
- 默认拒绝策略
-
分层防御体系
- Web 层:CSP + SameSite + 输入转义
- Bridge 层:来源校验 + 签名 + 时间窗 + nonce
- 网络层:HTTPS + Certificate Pinning
- 发布层:离线包签名 + 完整性校验 + 灰度熔断
-
可验证性
- 每个安全规则都有对应的自动化测试
- 每次敏感调用都有审计日志(脱敏)
- 每个异常都有 traceId 可回溯
-
持续改进
- 定期进行渗透测试和代码审计
- 建立漏洞响应流程(SLA < 24h for critical)
- 安全培训与文化建设
20.3 下一步行动清单
- 完成本文所有验收清单(第 11 章)
- 集成自动化安全扫描到 CI/CD(第 18 章)
- 部署实时监控 SDK(第 16 章)
- 制定安全应急响应预案(第 10 章)
- 每季度进行一次红队渗透测试
- 建立安全基线并纳入技术规范
20.4 延伸阅读
官方标准与规范:
- OWASP Mobile Application Security - 移动安全验证标准
- Android App security best practices - Android 安全最佳实践
- Apple Platform Security - iOS 安全白皮书
技术深度文章:
- WebView Security: A Deep Dive - WebView 安全深度分析
- CSP Is Dead, Long Live CSP - Google 关于 CSP 的研究
- Certificate Pinning Best Practices - 证书 Pinning 最佳实践
工具与框架:
- MobSF - 移动安全自动化扫描框架
- OWASP ZAP - Web 应用安全扫描工具
- Burp Suite - 渗透测试瑞士军刀