HarmonyOS 7 ArkWeb:隐私协议快照脚本剔除与导航拦截【鸿蒙心迹】
隐私协议页面最容易被忽略的不是排版,而是它到底在加载什么。线上 HTML 为了统计可能包含脚本,正文里的链接可能跳到第三方站点,外部图片可能在页面打开时就发起请求。如果应用只是把 URL 丢给 ArkWeb,页面能显示并不能证明加载边界清楚。
PolicyMirrorLab 把这个问题拆成构建时和运行时两道门。构建脚本用 node-html-parser 解析协议快照,删除脚本与危险属性,检查资源与链接域名,再生成本地 policy_sanitized.html。运行时由 PolicyPreviewPage 使用 ArkWeb 展示,同时通过 onLoadIntercept 继续守住导航白名单。
Demo 任务号 WEB-1731-284,演示时间 17:31,快照版本 privacy-2026.10.06,允许主机为 privacy.example.com 和 assets.example.com。清洗前有 25 项检查,已通过 17 项,页面展示 68%;清洗结果是删除 3 个脚本节点,拦下 2 个外域链接,状态 SAFE_PREVIEW。这些数据是用于说明流程的演示合同,不冒充真实审核记录。

一、把协议页看成可执行输入,问题就不只是“能不能打开”
协议 HTML 来自内容管理后台时,一次无意的模板变更就可能加入 <script>、内联 onclick或新的外部资源。对普通浏览器来说,这些是页面能力;对应用内的“隐私说明”来说,它们可能超出用户预期。
仅把 javaScriptAccess(false) 设为 false 不足以完成内容审计。它能缩小执行面,却不会自动把快照中的外域图片、表单、自动跳转标记或危险链接变成安全内容。相反,如果只做构建时清洗,运行时点击导航又缺少拦截,用户仍可能离开已审阅的快照。
因此 Demo 不试图用一个开关包办所有安全。构建时负责“产物内容可审计”,运行时负责“导航行为不逸出”。两层共用同一份域名规则,但不共用隐含状态。
node-html-parser 是一个生成简化 DOM 树的高速 HTML 解析器,支持 querySelectorAll()、属性读写、节点删除与 toString()。它对部分错误 HTML 有容错,但官方也明确说明不是所有恶意或严重错误的输入都能按浏览器方式解析。本文因此把它放在 Node 构建脚本中,不宣称它是 ArkTS 页面运行时依赖。
二、先让解析器生成一份可复核的清洗报告
第一段代码解决脚本节点、内联事件与 javascript: 链接混在正文中的问题。它在 Hvigor 任务调用的 Node 脚本中运行,输入是下载后的快照文本,输出是清洗后 HTML 和报告。
import { parse } from 'node-html-parser';
type SanitizeReport = {
removedScripts: number;
removedHandlers: number;
blockedLinks: number;
};
export function sanitizePolicyHtml(source: string):
{ html: string; report: SanitizeReport } {
const root = parse(source, { comment: false });
const report: SanitizeReport = {
removedScripts: 0, removedHandlers: 0, blockedLinks: 0
};
root.querySelectorAll('script').forEach((node) => {
node.remove();
report.removedScripts += 1;
});
root.querySelectorAll('*').forEach((node) => {
Object.keys(node.attributes).forEach((name) => {
if (name.toLowerCase().startsWith('on')) {
node.removeAttribute(name);
report.removedHandlers += 1;
}
});
const href = node.getAttribute('href');
if (href?.trim().toLowerCase().startsWith('javascript:')) {
node.setAttribute('href', '#blocked-link');
report.blockedLinks += 1;
}
});
return { html: root.toString(), report };
}
这段代码不把“解析成功”当成“内容安全”。报告保留删除数量,并与预期基线比较。如果上一版没有脚本,这一版突然删除 3 个,构建可以直接阻断或转人工复核,而不是默默产出一份看似干净的文件。
querySelectorAll('*') 是为了举例说明属性扫描;对很大的协议文档,可以在性能测试后改为显式标签集。更重要的是,不要用正则表达式替代 HTML 树解析;属性引号、换行、大小写和实体编码会很快把单条正则变成缝补集合。
危险属性删除后仍要保留原始快照,但原始文件不进入应用包。仓库里保留版本、获取时间与来源 URL,构建产物只包含清洗文件和脱敏报告。这样出现误删时能回放,又不把未处理输入变成运行时攻击面。
三、域名白名单要解析 URL,不要用字符串后缀猜
第二段代码解决 good.example.com.evil.test 之类的伪装域名。它统一将相对链接解析到协议主页,只允许 HTTPS,并用 URL.hostname 做精确比较。对不允许的链接,构建时改写为本地拦截锚点。
const ALLOWED_HOSTS = new Set([
'privacy.example.com',
'assets.example.com'
]);
function normalizeAllowedUrl(raw: string, base: string): string | null {
try {
const url = new URL(raw, base);
if (url.protocol !== 'https:' || !ALLOWED_HOSTS.has(url.hostname)) {
return null;
}
url.hash = '';
return url.toString();
} catch {
return null;
}
}
export function rewriteLinks(rootHtml: string): string {
const root = parse(rootHtml);
root.querySelectorAll('a').forEach((anchor) => {
const href = anchor.getAttribute('href') ?? '';
const safe = normalizeAllowedUrl(href,
'https://privacy.example.com/current/');
if (safe === null) {
anchor.setAttribute('href', '#blocked-link');
anchor.setAttribute('data-blocked', 'true');
} else {
anchor.setAttribute('href', safe);
}
});
return root.toString();
}
白名单要根据资源类型再分层。协议正文的点击链接可能只允许 privacy.example.com,图片与样式才允许 assets.example.com。示例为了聚焦主线共用一个集合,实际项目应将 navigationHosts、imageHosts 和 styleHosts 分开,防止一个静态资源域被意外获得导航资格。
还要处理 mailto:、tel: 和应用自定义 Scheme。它们不是 HTTPS,因此本文默认阻断。如果产品确实需要,应进入独立的用户确认流程,输入参数另做校验,而不是把它们塞进 HTTPS 白名单。
工程中,tools/policy-sanitize.ts 负责构建清洗,resources/rawfile/policy_sanitized.html 是唯一进包文件,pages/PolicyPreviewPage.ets 负责显示,model/PolicyAudit.ets 保存版本、检查数和状态。下面 DevEco Studio 风格配图中,右侧模拟器显示 68% 检查进度和 SAFE_PREVIEW,底部日志显示删除 3 个脚本与拦截 2 个外域链接。该图是生成式说明图,不是真实 IDE 截图。

四、ArkWeb 在运行时重新问一次“这个 URL 可以去吗”
构建产物通过后,页面仍然要保留运行时拦截。ArkWeb 的 onLoadIntercept 在 loadUrl 和 iframe 加载时触发,返回 true 表示取消本次导航,返回 false 表示允许。默认是允许,因此回调里的异常不能直接放过。
import { webview } from '@kit.ArkWeb';
@Entry
@Component
struct PolicyPreviewPage {
private controller: webview.WebviewController =
new webview.WebviewController();
@State blockedUrl: string = '';
private isAllowed(raw: string): boolean {
try {
const parsed = new URL(raw);
return parsed.protocol === 'https:' &&
(parsed.hostname === 'privacy.example.com' ||
parsed.hostname === 'assets.example.com');
} catch {
return raw.startsWith('resource://RAWFILE/');
}
}
build() {
Web({ src: $rawfile('policy_sanitized.html'),
controller: this.controller })
.javaScriptAccess(false)
.onLoadIntercept((event) => {
const url = event?.data.getRequestUrl() ?? '';
const blocked = !this.isAllowed(url);
if (blocked) this.blockedUrl = url;
return blocked;
})
}
}
这段代码中的 resource://RAWFILE/ 是为本地入口保留的示例分支,具体 URL 形式应以当前 ArkWeb 实际回调值为准,通过真机日志固化后再收窄规则。不能为了让本地页能打开,简单对所有非 HTTPS URL 返回 false。
onLoadIntercept 与 onOverrideUrlLoading 的触发时机不同。本文选前者,是因为官方说明它覆盖 loadUrl 和 iframe 加载。如果项目同时使用两个回调,要保持同一个决策函数,否则同一 URL 可能在一条路径被拦、另一条路径被放行。
页面不在拦截回调中自动打开替代 URL。自动重定向容易制造循环,也会把一次被阻断的行为伪装成正常访问。Demo 只写入 NAV_BLOCKED、记录脱敏后的主机与路径类型,由页面给出“返回协议”按钮。
手机运行图显示快照版本、允许域名、17/25 项检查、68% 进度与 SAFE_PREVIEW。红色细圈标出“脚本 3 个已删除”,说明页面展示的是清洗产物,而不是原始 HTML。

五、检查进度和内容版本必须成对,否则 68% 没有语义
页面不应该只接收一个百分比。PolicyAudit 快照至少包含 policyVersion、sourceFetchedAt、totalChecks、passedChecks、removedScripts、blockedLinks、allowedHosts和 state。当 policyVersion 变化时,旧检查数立即失效,不允许把新 HTML 和旧报告组合成一个绿色页面。
Demo 的 25 项检查不是官方审核清单,而是工程内部规则集。它包括脚本、内联事件、危险 Scheme、资源域名、链接域名、表单、iframe、meta refresh、内联样式与本地入口等。本文在 17 项通过时显示 68%,不意味着可以提审;只是为了与配图保持一致的调试快照。
正式流程里,未通过项应分为“可自动修复”与“必须人工确认”。删除 <script> 可以自动执行,但如果脚本原本负责展开折叠的必要正文,删除后内容可能不完整。所以构建不能只看清洗工具退出码,还要做标题、章节和关键段落的内容基线比对。
详情图展示一次外域导航被拦截的时间线:17:31:18 用户点击链接,目标主机 tracker.example.net,不在两个允许主机中,onLoadIntercept 返回 true,页面保持 SAFE_PREVIEW。红圈和箭头只标解决策点,不把整张图变成告警板。

六、校验要同时覆盖构建产物和运行时导航
构建测试的第一组输入是标准协议 HTML,期望文本、标题和合法链接保留。第二组加入大小写混合的脚本标签、单引号内联事件和编码后的 javascript:,检查解析后 DOM 是否真正清除,而不是只在字符串中搜一次。
第三组覆盖域名混淆:privacy.example.com.evil.test 必须被拦,大小写和默认端口应按 URL 规则归一。第四组对报告做快照测试,防止删除数量、版本号与输出文件错配。第五组在 ArkWeb 中注入允许和禁止 URL,验证回调返回值、页面状态和诊断日志一致。
还要专门测试渲染进程异常退出。官方建议在 onRenderExited 中释放系统资源、保存关键数据,需要恢复时再调用 loadUrl 加载。恢复不应该绕过白名单,也不应自动切回未清洗的线上页。本地快照是恢复输入,而不是安全检查的例外通道。
最后要在真机上核对主 frame、iframe、重定向、网络断开与 SSL 异常路径。生成式配图只能证明文图数据合同一致,不能替代 ArkWeb 的真实回调顺序和网络行为验收。
发布前还要将构建报告与安装包内的 policy_sanitized.html 做内容摘要对账。否则即使清洗任务在 CI 中通过,也无法证明打包实际携带的就是那份产物。对账记录应包含协议版本、允许域名集版本、清洗工具版本和产物摘要,但不把原始未清洗 HTML 混入应用运行资源。
七、一份可阅读的协议,也应该是一份可追溯的产物
构建时清洗的价值,是让每个发布包都对应一份固定的协议内容、一份清洗报告和一个版本号。运行时拦截的价值,是让用户从这份已审阅内容出发时,不会因一个链接悄然进入另一份未审阅内容。
WEB-1731-284 的 68% 并不是一个好看的进度条,而是明确告诉开发者:25 项内部检查只通过 17 项,尚不可用它宣称“审核已通过”。工程可以先用 SAFE_PREVIEW 展示已清洗快照,但交付门禁要等所有必须项结清。
这样的预览才既可阅读,又能说清自己的边界。
参考资料:
更多推荐





所有评论(0)