Cordova 鸿蒙 WebView 安全实战:能改的运行时拦,改不了的构建期审
导航闸门怎么接、引擎里改不了的安全属性怎么办、JS 门面的静默降级怎么防——这篇文章用一个 Cordova / HarmonyOS 安全基线插件,把这三个问题一次讲透。
仓库地址:https://atomgit.com/lqjmac/cordova-plugin-harmony-webguard
版本地址:https://atomgit.com/CPF-Cordova
三方库框架:https://atomgit.com/oh-tpc
0. 环境与实测设备
| 项 | 版本/型号 |
|---|---|
| DevEco Studio | 26.0.0.851(HarmonyOS SDK 7.0.0 / API 26) |
| Cordova | v14.0.2 |
| hcordova | 1.0.7(CPF-Cordova 鸿蒙社区提供) |
| Cordova OHOS 引擎 | @cordova-ohos/ohos 14.0.2 |
| cordova-plugin-harmony-webguard | 1.0.0(本仓库) |
| Node.js | 20.19.5(审计 CLI 与 node:test) |
| 测试 | node:test 19 项:URL 策略、审计规则命中、config.xml 通配边界、空工程 fail-closed、CLI 退出码、Markdown 输出 |
| 实测环境 | HarmonyOS 2in1 模拟器(API 26) |
1. 页面能不能加载,容器说了算——这个问题是怎么找上我的
起因其实很简单。给一个 Cordova 鸿蒙应用做安全自查,我的第一反应跟大多数人一样朴素:Cordova 应用的页面不就是自己 www/ 里那点 HTML 吗,加载什么自己控制,能有什么风险?直到我在测试页的链接里塞了一个 javascript:alert(1),又把启动页临时换成 file:// 路径——两个都加载成功了。这时我才意识到,Cordova 应用跑在 ArkWeb 里,WebView 对任何导航来者不拒:window.location 改一下、业务代码拼一个 URL、甚至被注入一段脚本发起的跳转,容器都不会过问。
往深处查才发现一个更根本的问题:Cordova OHOS 引擎把 Web 组件的安全属性——mixedMode、databaseAccess、setPathAllowingUniversalAccess、客户端证书的默认 confirm——全部硬编码在引擎源码 MainPage.ets 里。我原以为写个插件运行时改掉就行,实际上这些属性在 Web 组件创建期就固定了,插件 API 根本够不着。
所以这个题目的边界反而清晰了:能改的,运行时拦;改不了的,构建期审。具体拆成三件事:
- 导航闸门怎么接(
onPageStart+ 策略引擎 + JS 预检查,这是核心); - 引擎里改不了的安全属性怎么办(构建期扫描
MainPage.ets/module.json5/config.xml,退出码卡 CI); - JS 门面怎么防静默降级(mirror、
isNative()、策略校验)。
下面是干完之后的完整记录,包括我踩的每一个坑。
2. 效果展示
按老规矩,先给效果再讲原理。示例应用叫 WebGuardDemo,页面顶部把定位写在标题下:WebGuard · Cordova / HarmonyOS WebView 安全基线,右上角一枚 ArkWeb 徽章。摘要一句话:"Cordova 应用跑在 ArkWeb 里,页面能不能加载由容器决定。这个页面演示三件事:导航前判定 URL、核对引擎配置红线、读构建期审计快照。"下面三个统计数字——3 高危、5 中危、4/8 用例通过——直接告诉你这个演示工程自己就没过审计。
第一个 Tab 是 URL 判定。四个策略开关加上一份实时判定记录:

开关区是策略的可视化:仅 HTTPS(明文 HTTP 直接拒绝)和禁止 IP 直连(避免绕过域名白名单)默认开启;允许 file://(只用于受控本地页)和允许混合内容(开启会拉低整页安全等级)默认关闭。下面的判定记录是这台模拟器上真实跑出来的——checked 8 · blocked 4,八次导航判定拦下四次:javascript:alert(1) 和 file:///data/storage/el2/base/secret.html 被 BLOCKED_SCHEME 拦下(协议在禁止列表),https://evil.example/phish 被判 HOST_NOT_ALLOWED(不在白名单),明文 http://app.example.com/home 也被拒——同一个主机,换个协议就过不去。页面底部一行小字交代了基线:WebGuard 1.0.0 · Cordova 14.0.2 · HarmonyOS API 26。
第二个 Tab 是配置红线,展示的是运行时改不了、只能构建期审的那部分:

卡片标题"引擎里的配置红线",右上角标注构建期扫描 · MainPage.ets。说明文字把逻辑讲透了:"这些是 Cordova OHOS 引擎默认值里真实存在的配置。Web 组件属性在创建期固定,插件运行时改不了,所以统一放到构建期审计,用 --fail-on high 卡在 CI。"四条命中逐条列出:WG-WEB-001 高危——混合内容全放行(MainPage.ets:641),建议改为 MixedMode.None;WG-WEB-002 高危——setPathAllowingUniversalAccess 允许 file:// 访问应用沙箱(:502),建议删除该调用;WG-WEB-003 高危——客户端证书请求被默认确认(:232),金融类页面必须走双向认证白名单;WG-WEB-004 中危——Web SQL/IndexedDB 常开(:642)。每条都精确到文件与行号——这是静态扫描,不是泛泛的安全建议。
3. 闸门与红线:运行时和构建期的分工
写代码之前得先把原理捋好。
整个插件的设计哲学就是标题那句话,链路分成两条:
运行时(插件做得到)
JS 侧:WebGuard.checkUrl(url) ← 导航前预检查
│
▼
ArkWeb onPageStart(每次主框架导航广播)
│ WebGuardPlugin:取当前 URL → 策略引擎同步判定
├─ ALLOW → 放行
└─ 拒绝 → 立即 stop(),code 记入判定记录
构建期(插件做不到,审计 CLI 补位)
node lib/cli/webguard-audit.js --root . --fail-on high
├─ MainPage.ets → WG-WEB-001~012(引擎源码里的安全属性)
├─ module.json5 → WG-PRM-001(高敏权限声明)
└─ config.xml → WG-CFG-001/002(allow-intent / allow-navigation)
└─ findings 达到阈值 → 退出码 2,CI 失败
先说运行时这条。策略判定本身是纯同步的字符串操作——解析 scheme/host、查禁止列表、查白名单、拦 IP 与 Punycode——没有任何网络和 IO,单次判定微秒级,挂在每次导航的 onPageStart 上毫无压力。策略引擎写成纯函数还有一个收益:同一份规则既能用在原生 onPageStart,也能用在 JS 侧的预检查(类注释原话),页面在改 location 之前就能知道这次跳转会不会被拦。
再说构建期这条,它是整个设计的诚实之处。mixedMode(MixedMode.All) 这类属性写在引擎源码里,Web 组件创建期就固定了——插件在运行时假装能改它,是很多"安全插件"的通病。WebGuard 的做法是明确在能力边界表里写"不能":不能修改创建期属性、不能关闭引擎已调用的 setPathAllowingUniversalAccess、不能自动修复引擎源码——改不了的,交给审计 CLI 扫源码出报告,用退出码逼着构建管线去改。
顺手记一笔性能账:运行时闸门是纯字符串判定,微秒级,不碰网络;审计 CLI 扫的是几个文本文件加十几条正则规则,秒级完成。真正值钱的成本在设计上——把"能改"和"不能改"分清楚,比多写十条规则都重要。
各条路线对照一下:
| 防护方式 | 时机 | 鸿蒙 Cordova 工程的现状 |
|---|---|---|
config.xml 白名单 | 配置期 | Cordova 内建机制,但引擎实现各异、粒度粗 |
| 各应用手写 onPageStart 拦截 | 运行时 | 逻辑散落在业务代码里,没有统一策略 |
| 引擎 MainPage 硬编码属性 | 引擎期 | 安全属性固定值,运行时不可改 |
| 本库运行时闸门 | 运行时 | 统一策略引擎:协议 / 白名单 / IP / Punycode / 长度 |
| 本库构建期审计 | 构建期 | 12 条源码规则 + 权限 + config.xml,退出码卡 CI |
能力边界照抄 README 那张表的一半(“不能"的那一半):修改 Web 组件创建期的 mixedMode、databaseAccess——不能;关闭引擎已调用的 setPathAllowingUniversalAccess——不能;修改客户端证书默认 confirm 分支——不能;自动修复引擎源码——不能。了解了这个边界,你就知道这个插件不承诺"一键安全”,它承诺的是把安全状态变成可见、可测、可卡 CI 的东西。
4. 工程怎么搭:一个纯策略内核,三副面孔
先看整体结构。这个插件分三层:JS 门面给业务,ArkTS 策略引擎给原生,审计 CLI 给 CI:
cordova-plugin-harmony-webguard/
├─ plugin.xml
├─ lib/
│ ├─ webguard.js # JS 门面 + 本地 mirror + isNative()
│ └─ cli/webguard-audit.js # 构建期审计 CLI(fail-closed)
├─ ohos/
│ └─ src/main/ets/
│ ├─ WebGuardPolicy.ets # 纯策略引擎(无 Cordova 依赖)
│ └─ WebGuardPlugin.ets # CordovaPlugin:onPageStart 闸门 + checkUrl
├─ test/ # node:test 19 项 + fixture 工程
├─ example/ # www + 宿主补丁 + setup.ps1 一键搭建
├─ docs/ # 配置红线文章与截图
├─ plugin.xml / package.json
└─ README.OpenHarmony.md / README.OpenHarmony_CN.md

这次设计最关键的决定有两个:
策略引擎是纯的,插件壳是薄的。 WebGuardPolicy.ets 不 import 任何 Cordova 类型——它只认识 URL 字符串和策略配置,checkUrl 进、CheckResult 出。类注释里写明这样做的收益:“同一份规则既能用在 onPageStart 导航拦截,也能用在 JS 侧的预检查”,而且它能被 node:test 直接测(第 6 节的 19 条回归测试里大半打在策略层)。WebGuardPlugin.ets 则只干两件事(类注释原文):“把策略引擎暴露给 JS(checkUrl / configure / getStats);在 onPageStart 生命周期里做最后一道 URL 闸门。”
审计宁可误杀不可漏杀。 退出码表里有一条刻意的设计:退出码 3 表示没有扫描到目标文件,默认失败,--allow-empty 才放行。README 把理由写得很重:"–root 指错、忘了 platform add harmonyos、引擎升级后 MainPage.ets 换了位置,都不能让 CI 变绿。"审计工具最危险的失败模式不是报错,是扫了个寂寞还显示通过。
还有一件事必须单独讲:Cordova OHOS 的 ArkTS 插件不是自动扫描注册的,宿主要有三步接线——把插件实例放进 PluginEntry 数组传给 MainPage、把插件源码复制到 cordova/src/main/ets/components/WebGuard/、在 cordova/Index.ets 追加导出:
// harmonyos/entry/src/main/ets/pages/Index.ets
import {
MainPage, PluginEntry, WebGuardPlugin,
pageBackPress, pageHideEvent, pageShowEvent
} from '@cordova-ohos/ohos/Index';
@Entry
@Component
struct Index {
private plugs: PluginEntry[] = [
{ pluginName: 'WebGuard', pluginObject: new WebGuardPlugin() }
];
build() {
RelativeContainer() {
MainPage({ cordovaPlugs: this.plugs });
}.height('100%').width('100%');
}
}
这套步骤被 example/scripts/setup.ps1 自动化了:建工程、加平台、复制 www/、复制插件源码、打 Index.ets 补丁,五步一条命令。对使用方来说,JS 侧的 API 是这样的:
await WebGuard.install({
allowedHosts: ['www.example.com', 'app.example.com', '*.trusted.example'],
httpsOnly: true,
blockIpHost: true,
allowFileAccess: false,
});
const result = await WebGuard.checkUrl('https://app.example.com/home');
// { allowed: true, code: 'ALLOW', host: 'app.example.com', rule: 'app.example.com' }
const blocked = await WebGuard.checkUrl('javascript:alert(1)');
// { allowed: false, code: 'BLOCKED_SCHEME', reason: '协议 javascript: 在禁止列表中' }
await WebGuard.navigate('https://evil.example'); // 先判定,拒绝则不跳转
5. 把代码讲透
5.1 策略引擎:code 是契约,reason 是给人看的
WebGuardPolicy.ets 的第一个类型定义就是这份契约:
// ohos/src/main/ets/WebGuardPolicy.ets(节选)
export interface CheckResult {
allowed: boolean;
code: string; // 'ALLOW' / 'BLOCKED_SCHEME' / 'HTTP_NOT_ALLOWED'
reason: string; // / 'HOST_NOT_ALLOWED' …稳定机器可读枚举
host: string;
scheme: string;
rule: string;
}
接口注释把用法规定得很死:“code 是稳定的机器可读枚举,方便在 CI 和 UI 里做断言,不要用 reason 做判断。”——reason 是中文提示文案,文案会改;code 不会。判定链的顺序就是风险优先级:先查 scheme 禁止列表(BLOCKED_SCHEME),再查 HTTPS(HTTP_NOT_ALLOWED),再查域名白名单(HOST_NOT_ALLOWED),全过才是 ALLOW。示例页判定记录里那四种结论,正好是这条链的四级采样。
默认策略(DEFAULT_POLICY)里藏着整个插件最重要的一条注释:
allowedHosts: ['www.example.com', 'app.example.com', '*.trusted.example'],
// www.example.com 是 Cordova OHOS 把 rawfile/www 映射出来的本地虚拟主机,
// 不是公网域名;不放行它,应用自己的首页会被 onPageStart 拦掉。
白名单必须包含 Cordova 的本地虚拟主机——它长得像个公网域名,其实是自家 rawfile/www 的映射。漏掉它,插件一装上首页就白屏(坑一)。*.trusted.example 这种限定在具体域名下的通配是允许的,审计 CLI 对 config.xml 的通配判定也遵守同一条边界。
5.2 onPageStart 闸门:最后一道,不是唯一一道
WebGuardPlugin 在 initialize 里先干一件与策略无关但越早越好的事:
// ohos/src/main/ets/WebGuardPlugin.ets(节选)
initialize(cordovaInterface: CordovaInterface, cordovaWebView: CordovaWebView): void {
this.controller = cordovaWebView.getWebviewController();
// 生产包一律关闭 Web 调试;这行不依赖任何页面,越早执行越好。
try {
webview.WebviewController.setWebDebuggingAccess(false);
} catch (error) { /* … */ }
}
然后是核心闸门。onPageStart 注释把它的定位讲得很准:“Cordova 每次主框架开始导航都会广播 onPageStart。这里拿当前 URL 做一次同步判定;不允许就立刻 stop()。这是’最后一道闸’,不是唯一一道:JS 侧在改 location 之前也应先 checkUrl。”——最后一道闸的意思是它兜底,不是唯一的意思是它不做全部:被注入的脚本发起的导航它拦得住,但业务代码里明知会被拦还要硬跳的失败体验,得靠 JS 侧预检查提前避免。插件还维护一份 GuardStats(checked / blocked / lastCode / lastUrl),getStats 暴露给 JS——示例页判定记录顶部那行"checked 8 · blocked 4"的数据源就是它。
5.3 JS 门面:mirror、isNative 与静默降级的防线
浏览器里直接打开演示页时没有 Cordova 环境,lib/webguard.js 会退回一个本地 mirror——用同一套纯 JS 策略实现回答 checkUrl,让演示页在浏览器里也能跑。这本是便利,却藏着一个危险:如果真机上因为加载问题静默退回了 mirror,所有"原生策略"都成了假象。所以门面做了三层防护:
// 加载路径:require / cordova.require 双路径,拿不到 exec 不再静默 mirror
const result = await WebGuard.checkUrl('https://app.example.com/home');
if (!await WebGuard.isNative()) {
// 当前在用本地 mirror——排查"改了 native 没生效"先看这个
}
README 里 isNative() 的描述写得很直白:"false 表示在用本地 mirror(浏览器预览或 exec 不可用),排查’改了 native 没生效’时先看这个。"这个函数是坑三的直接产物。另外 install / configure 会先校验策略:未知字段忽略(向前兼容),类型、条数(≤200)或长度(≤253)不合法时返回 rejected Promise——脏配置在 JS 侧就被拒掉,不会带进 native(ArkTS 侧还有一层上限兜底,双保险)。
5.4 构建期审计:12 条源码规则与 fail-closed
lib/cli/webguard-audit.js 扫三类目标。引擎源码 MainPage.ets 上有 12 条 WG-WEB-* 规则,从高到低:001 混合内容全放行、002 setPathAllowingUniversalAccess(注明了触发条件——默认 https 虚拟主机下不触发,改用 file:// 启动页才生效)、003 客户端证书默认 confirm、012 Web 调试开启;清单 config.xml 上是 WG-CFG-*:allow-intent 指向明文 http 或通配主机、allow-navigation 放开任意站点(*、https://*/*、*.com)都判高危,而 https://*.trusted.example/* 这类限定具体域名的通配不误报,tel:* / mailto:* 也不在列;权限 module.json5 上是 WG-PRM-001 高敏权限声明。每条命中带文件路径与行号——示例页"配置红线"Tab 里那些 MainPage.ets:641 就是这么来的。
退出码是这条链路的牙齿:
| 退出码 | 含义 |
|---|---|
| 0 | 通过(无达到阈值的 finding) |
| 1 | 参数或路径错误 |
| 2 | 命中 --fail-on 阈值 |
| 3 | 没有扫描到目标文件(默认失败,--allow-empty 才放行) |
还有一条补丁规则:只找到部分文件(比如 MainPage.ets 缺失)时,报告会明确警告哪些 WG-WEB-* 规则没有执行,不会静默当成干净。审计的公信力就在这些细节里。
5.5 一键搭建:setup.ps1 与 audit:demo
示例工程把"从零到能跑"的全过程脚本化了:powershell scripts/setup.ps1 依次完成 hcordova create 建工程、platform add harmonyos、复制 www/、复制插件源码与 config.xml、给 Index.ets 和 cordova/Index.ets 打补丁。而 npm run audit:demo 扫描仓库内置的 test/fixtures/sample——一个带敏感权限与通配 intent 的故意"脏"工程,干净克隆即可复现 6 条高危(退出码 2 是预期)。演示不需要一个真实工程,fixture 就是可复现的证据——这个思路和供应链审计篇的演练样本一脉相承。
6. 我踩过的坑(每一条都有真实修复记录)
这一节可能是全文最值钱的部分。以下坑全部来自 CHANGELOG 1.0.0 修订记录里的真实修复。
坑一:插件一装上,首页白屏。 默认白名单漏了 www.example.com——它是 Cordova OHOS 把 rawfile/www 映射出来的本地虚拟主机,长得像公网域名,实际是自家首页。它不在白名单里,首页第一次导航就被自家闸门拦掉。修复:默认策略内置它,并在代码注释里专门解释(见 5.1)。教训:安全策略上线前,先用自家首页过一遍闸门。
坑二:审计扫了个寂寞,CI 还是绿的。 最初的审计器 fail-open——--root 指错、忘了 platform add harmonyos、引擎升级换目录,扫到 0 个目标文件就输出"通过"。修复:fail-closed——扫不到任何目标文件时报告 empty、passed=false,CLI 返回退出码 3,需要 --allow-empty 才显式放行;部分文件缺失时还要在报告里警告哪些规则没执行。审计工具最危险的失败不是报错,是没干活却报了平安。
坑三:真机上策略"没生效",其实跑的是 mirror。 示例页以普通 <script> 加载 webguard.js 时,require("cordova/exec") 不可用,门面静默退回本地 mirror——JS 层看一切正常,原生闸门根本没参与。修复:加载改为 require / cordova.require 双路径,新增 WebGuard.isNative() 区分 native 与 mirror。降级可以,静默降级不行——必须给调用方一个"你现在在哪条腿上"的查询。
坑四:config.xml 的通配,误报与漏报两头堵。 一开始 *、https://*/*、*.com、app.*.com 这类放开任意站点的写法没全覆盖;反过来又要保证 https://*.trusted.example/*(限定具体域名的通配)不误报、tel:* / mailto:* 不在列。修复:通配判定按"是否放开到任意主机"划线,并配了边界单测。
坑五:Punycode 只拦了顶级域,子域标签绕过了。 www.xn--... 这种子域标签形式的同形异义域名校验没覆盖。修复:Punycode 拦截下沉到子域标签粒度,JS 门面与 ArkTS 策略两侧同步改——策略改一处,两侧必须同时动,这也是策略引擎抽成纯函数的另一个理由:对照起来容易。
坑六:修复建议写得想当然。 WG-WEB-002(setPathAllowingUniversalAccess)最初的建议是"删除该调用",但默认 https 虚拟主机模式下这个调用并不触发,只有改用 file:// 启动页才生效——按原建议改,多数工程白改。修复:建议里注明触发条件。审计规则的修复建议必须写清楚适用条件,不然就是噪音。
坑七:脏配置一路带进 native。 install / configure 最初不校验,allowedHosts 传个非数组也能过。修复:JS 侧校验类型、条数(≤200)、长度(≤253),不合法返回 rejected Promise;ArkTS 侧保留上限兜底。配置是策略的输入,输入不干净,策略输出的一切都是自欺。
7. 现在怎么用:从一条命令到 CI 卡点
经过上面的整理,这个工程是"运行时插件 + 构建期审计"双形态交付(插件 ID 见 plugin.xml,版本 1.0.0,Apache-2.0)。安装:
# 本地插件
hcordova plugin add ../cordova-plugin-harmony-webguard
# 或从 AtomGit
hcordova plugin add https://atomgit.com/lqjmac/cordova-plugin-harmony-webguard.git
安装后确认 harmonyos/entry/src/main/resources/rawfile/config.xml 出现 WebGuard 的 feature 声明,再按第 4 节完成三步注册(或直接跑 example/scripts/setup.ps1 一键搭建)。运行时闸门即装即用;构建期审计进 CI:
# 扫当前 Cordova 工程(含 harmonyos/),高危命中即失败
node lib/cli/webguard-audit.js --root . --format md --fail-on high
# 输出 JSON 归档
node lib/cli/webguard-audit.js --root . --format json --out build/security/webguard.json
# 干净克隆复现:扫内置 fixture,6 条高危,退出码 2 是预期
npm run audit:demo
完整构建示例应用:cd harmonyos && ohpm install --all && hvigorw assembleHap --mode module -p product=default -p buildMode=debug --no-daemon,再 hdc install(注意工程路径不要含中文,hvigor 会拒绝)。更完整的规则表、退出码语义和接线说明在仓库的文档里:
README.OpenHarmony_CN.md:中文,能力边界、注册接线、规则表与退出码语义;README.OpenHarmony.md:英文,内容与中文版一一对应;docs/Cordova鸿蒙WebView安全配置红线.md:配置红线的专题文章与截图;test/:node:test 19 项回归 + fixture 工程。
当前版本在 Cordova 14.0.2 / hcordova 1.0.7 / Cordova OHOS 引擎 @cordova-ohos/ohos 14.0.2、DevEco Studio 26.0.0.851(HarmonyOS SDK API 26)环境下验证:npm test 19 项全绿——覆盖 URL 策略(HTTPS、白名单、*. 子域、IP、Punycode、凭据、长度、参数校验)、审计规则命中、config.xml 通配边界、空工程 fail-closed、CLI 退出码与 Markdown 输出;示例 WebGuardDemo 在 2in1 模拟器实测:URL 判定 checked 8 · blocked 4(javascript:、file://、明文 http、站外域名四种拦截全部命中),配置红线 Tab 读出构建期审计快照 3 高危 5 中危,每条精确到引擎源码行号。能改的运行时拦、改不了的构建期审——这两句话就是整个插件的安全模型,也是它对自己能力边界最诚实的表达。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)