HarmonyOS Node.js fs + path:三方HAR软链接越界与缺失依赖拦截【鸿蒙心迹】
同一个HarmonyOS工程,在开发者电脑上能找到本地HAR包,换一台构建机器却报找不到文件。问题有时不是ohpm网络仓库,也不是版本号写错,而是仓库里的某个 .har 路径实际指向了仓库之外。Linux和macOS环境允许创建软链接,CI拉取源码时未必保留链接目标;开发电脑恰好有那个目标,就会掩盖交付缺口。这次把问题缩到一种明确输入:oh-package.json5 的dependencies里声明的本地 file:*.har。演示项目LinkFence / HAR-2808要做的是构建前预检,不是替代ohpm安装器或修改任何锁文件。

一、区分声明路径和真实路径
仅对字符串做 ../ 检查并不充分。假设模块目录是 entry/,允许的共享包目录是仓库内的 libs/,配置 file:../libs/theme.har 看起来完全合理;然而 theme.har 可以是一个符号链接,真正指向机器上的其他目录。字符串看上去没越界,实际读取的却是不可移植文件。反过来,合法路径中出现 .. 并不意味着恶意,模块从entry/引用兄弟目录本来就需要回退一级。
因此需要比较两种路径:path.resolve()把声明的相对路径解析为词法绝对路径,fs.realpath()进一步解析软链接得到真实目标。最后让真实目标与经过真实路径解析的允许目录相比较。校验规则很简单:目标必须存在、是普通文件、扩展名符合本次限定的HAR输入,并且真实目标必须位于允许的libs/目录内。只有满足这些约束,才标记为PASS。这属于团队自己定义的交付规范,并不是华为SDK要求所有本地包都只能放在libs/。
另外应当承认范围有限。ohpm支持本地文件、目录、远程包和模块引用,dynamicDependencies也有专门的适用范围。本工具不解析远程依赖图,不校验.tgz内容,更不会直接改写oh-package-lock.json5。它的目标是补上“本地HAR交付可搬迁性”这一道早期检查,让明确的构建风险在CI里可读、可定位。
二、先限定扫描对象,不做正则猜测
官方文档说明,oh-package.json5是JSON5格式,可在模块的dependencies和其他依赖字段声明依赖;本地HAR示例可使用file:../package.har。真实文件可能包含注释和尾随逗号,不能无条件用内置JSON.parse处理所有JSON5输入。这里明确依赖Node.js侧可安装的json5解析器(例如开发依赖执行npm install --save-dev json5),解析完成后只读取生产依赖中的.har文件项。代码执行于桌面或CI的Node.js,不需要把Node.js模块搬进ArkTS应用包。
// scan-local-har.mjs:Node.js工具端执行
import fs from 'node:fs/promises';
import path from 'node:path';
import JSON5 from 'json5';
async function localHarSpecs(manifestPath) {
const raw = await fs.readFile(manifestPath, 'utf8');
const data = JSON5.parse(raw);
const deps = data.dependencies ?? {};
return Object.entries(deps)
.filter(([, spec]) => typeof spec === 'string')
.filter(([, spec]) => spec.startsWith('file:') &&
spec.toLowerCase().endsWith('.har'))
.map(([name, spec]) => ({ name, spec }));
}
这段代码把输入和规则拆开,先获得name与spec,不必让后续审计函数理解整个包清单。遇到依赖字段不是字符串的情况,当前实现跳过,生产版最好额外报告INVALID_SPEC,避免格式异常被误认为正常。如果团队确实把本地HAR放在开发依赖或额外路径中,可以显式扩展扫描列表,而不是偷偷把每类包都按同一交付策略处理。解析器安装失败时应让CI直接报错,不要在catch中返回空依赖列表。
三、解析软链接后再做目录边界判定
本次业务规则把libs/作为允许边界。使用path.relative()判断真实目标相对于真实允许目录的位置,比直接比较字符串前缀安全,因为/workspace/libs-old虽然以/workspace/libs开头,却并不在该目录内。下面将不存在、越界、类型不符拆成不同状态。示例假定配置由可信代码库维护;面对不可信贡献者,还要额外考虑校验到消费之间被替换的竞态。
async function auditHar(entry, moduleDir, allowedDir) {
const declared = path.resolve(moduleDir, entry.spec.slice(5));
let target;
try {
target = await fs.realpath(declared);
} catch (err) {
if (err?.code === 'ENOENT') return { name: entry.name, status: 'MISSING' };
throw err;
}
const root = await fs.realpath(allowedDir);
const relative = path.relative(root, target);
if (relative === '..' || relative.startsWith(`..${path.sep}`) ||
path.isAbsolute(relative)) {
return { name: entry.name, status: 'SYMLINK_OUTSIDE' };
}
const info = await fs.stat(target);
if (!info.isFile()) return { name: entry.name, status: 'NOT_FILE' };
return { name: entry.name, status: 'PASS' };
}
realpath会解析最终目标,stat也针对解析后的文件工作。返回状态里的SYMLINK_OUTSIDE在这里是一种简化命名:无论越界由软链接还是绝对路径造成,都会归入这个阻断原因;正式系统可以另加PATH_OUTSIDE细分。如果存在损坏链接,ENOENT归为缺失,便于构建负责人快速补齐。注意校验通过不代表这个HAR包本身没有恶意代码,也不代表它与目标SDK兼容;内容签名、版本依赖和安全审计是独立环节。
四、用三种输入把门禁结果讲清楚
在固定样例中,仓库有真实文件libs/vision.har,有一个名为libs/theme.har的软链接实际指向/outside/theme.har,还有一个不存在的libs/chart.har。相应的依赖标识为@demo/vision、@demo/theme与@demo/chart。测试时不需要下载任何外部HAR,创建少量占位二进制即可验证路径算法;但这种测试只证明路径检查行为,不能证明HAR包格式合法。尤其要防止把测试用的空文件带入真实发布产物。
const moduleDir = path.resolve('sample/entry');
const allowedDir = path.resolve('sample/libs');
const manifestPath = path.join(moduleDir, 'oh-package.json5');
const rows = [];
for (const entry of await localHarSpecs(manifestPath)) {
rows.push(await auditHar(entry, moduleDir, allowedDir));
}
for (const row of rows) console.log(`${row.name} ${row.status}`);
const pass = rows.filter(x => x.status === 'PASS').length;
const blocked = rows.some(x => x.status !== 'PASS');
console.log(`${pass}/${rows.length} PASS · ${blocked ? 'BLOCKED' : 'READY'}`);
if (blocked) process.exitCode = 1;
// @demo/vision PASS
// @demo/theme SYMLINK_OUTSIDE
// @demo/chart MISSING
// 1/3 PASS · BLOCKED
process.exitCode使CI能够识别失败,而不是只在日志里打印一行红字就继续发布。真实脚本的工作目录也必须明确,示例从项目根目录运行;CI切换目录后,应把工程根目录作为命令行参数传进来,不要默默使用另一个sample/。为了减少噪声,可以按依赖名排序输出,并将绝对路径放到只对内部维护者可见的详细日志,公开报告保留相对路径即可。发现越界时应修复清单或把文件正式入库,而不是为了通过门禁临时扩大允许目录到整个磁盘。

五、把门禁结果变成能读懂的界面
如果团队准备做一个仅供审阅的HarmonyOS本地预览页,可以将CI输出的三行状态作为静态JSON展示。图中合成页面标题LinkFence,任务号HAR-2808,大卡片展示1/3 PASS · BLOCKED;列表里@demo/vision为绿色,@demo/theme为橙色,@demo/chart为红色。开发环境图的中间编辑器显示fs.realpath()与path.relative()这一核心规则,底部HiLog样式文本回放相同结果。必须说明:Node脚本在PC或CI执行,手机演示页只负责呈现结果,不是在HarmonyOS设备上执行Node文件系统访问。

还需要准备几组反例做回归:其一,正常相对路径从entry/回退到libs/,确保不会误杀;其二,目标位置写为libs-old/,验证不是字符串前缀判断;其三,链式软链接最终逃出允许目录,验证realpath可以解析链;其四,链接指向目录而非HAR文件,验证NOT_FILE;其五,权限不足等非ENOENT异常必须进入错误路径,而非被沉默地判为缺失。Windows下还需要考虑路径大小写、驱动器及符号链接权限,不能把单一平台的测试结果外推到所有CI机器。
除了终端日志,建议同时让脚本生成机器可读的汇总对象,字段只需包含包名、声明路径、校验状态和错误类别。一个轻量的构建流水线便能据此给出醒目的失败原因,而不是要求维护者翻阅数百行安装输出。日志还应避免泄露开发者家目录的绝对路径:本地排查可以显示真实路径,上传到公共CI报告时则只保留仓库相对位置和必要的匿名错误码。对于同一个HAR被多处引用的情况,可缓存本次校验结果,但不要把缓存跨越不同构建任务长期保存,因为符号链接目标可能已变化。
更进一步,最好把合法样例、缺失样例和越界样例固定为仓库中的自动测试资源,测试时在临时目录创建符号链接,测试结束删除整个临时目录。这样脚本修改后仍能验证退出码约定没有改变。真实构建阶段应在隔离工作目录重新运行扫描,避免开发机器上的绝对路径影响结果。通过校验之后再执行官方包管理命令,而不是拿通过状态来推断依赖已经安装。
六、理解它能挡住什么,挡不住什么
真正要封住的不是../这两个字符,而是构建过程隐含的“这台电脑恰好有”依赖。目录边界规则建立在代码仓库可版本管理的资产组织约定上,能帮助团队发现缺图、缺包同类问题里的文件依赖部分,也方便审计人员定位谁在引用工作区之外的东西。它不负责修改ohpm的解析逻辑,不检查远程包供应链,也不能完全消除TOCTOU竞态;高安全场景应在隔离构建容器中重新解析并复制允许的产物,随后对内容做散列与签名验证。
本文提供的是具有明确输入和退出码的Node.js预检方案,配套开发界面和手机页面是合成示意。把工具插到ohpm install或打包之前能早发现风险,但正式上线仍需依照当前SDK、构建方式和项目内部目录规范验证。开发和发布阶段若需要复现完整依赖树,应通过官方ohpm命令与锁文件管理流程解决,而不是直接用自定义脚本篡改锁文件。
参考资料:华为开发者《oh-package.json5》https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/ide-hmos-oh-package-json5 ;《添加依赖项》https://developer.huawei.com/consumer/cn/doc/HarmonyOS-Guides/ide-hmos-hvigor-dependencies 。
更多推荐

所有评论(0)