HarmonyOS Node.js:HAR资源路径大小写与越界校验【共创稿事节】
在三方HAR模块里,资源路径看起来只是一个字符串,但它连接着依赖包、工程目录和打包环境。如果研发机器上的文件系统不区分大小写,引用img/Banner.png也许还能打开;换到另一种环境时,实际文件名是banner.png,就可能在构建或运行中暴露问题。还有一种更隐蔽的风险:资源清单被人工拼接后带入../,读取目标已经不在模块允许范围内。这个检查不需要等待整个应用打包失败,Node.js本身就足以提前把问题清单打印出来。

一、哪些事情由官方依赖工具负责,哪些是我们的自检
HarmonyOS工程通过ohpm管理三方包,模块级oh-package.json5描述包名、版本和入口,HAR可以作为共享模块分发。官方文档说明了本地HAR引用与安装流程,也说明依赖项最终位于相应模块的oh_modules目录。这里不去修改ohpm解析行为,更不替代工具的官方合法性检查,而是给团队自己维护的“待打包静态资源清单”增加一个保守检查步骤。把它称为PathGate。
输入是数组,每条记录必须含业务ID与一个相对于third_har/resources的路径。构造目录里有img/icon.png和img/banner.png,此外还有位于资源根目录之外的private/key.txt。HAR-101引用正确文件,HAR-102故意写成img/Banner.png,HAR-103故意写成../private/key.txt。我们希望得到PASS、CASE_MISMATCH、ESCAPE_ROOT,最终PASS 1/3 BLOCKED。这组文件只是为Node脚本准备的测试素材,不是某个真实HAR生产包。
为什么同时检查大小写和越界?因为二者发生在不同层面。大小写错误是资源定位可靠性问题,路径越界是读取边界问题,若统一报“找不到文件”,开发者可能在清单中反复补路径,反而掩盖潜在的安全风险。为此每项错误都采用稳定短码,并与原始引用ID一起输出;构建流水线按退出码决定是否继续,开发者根据短码手动排查。
二、先把“允许根目录”定义清楚
下面第一段代码只做数据契约和静态路径边界判断。它不是鸿蒙设备端ArkTS代码,而是运行在开发机或CI里的Node.js TypeScript风格脚本。真实运行可以用支持TypeScript的工具链编译,或者将类型标注移除变成标准ESM。为什么不直接使用字符串startsWith判断目录是否属于某个根?因为/path/a和/path/ab有共同前缀,却不在同一个目录层级。path.relative可以把文件系统路径关系转化成相对结果,再识别..前缀和绝对路径。
import * as fs from 'node:fs/promises';
import * as path from 'node:path';
interface AssetRef { id: string; file: string }
const moduleRoot = path.resolve('third_har/resources');
const cases: AssetRef[] = [
{ id: 'HAR-101', file: 'img/icon.png' },
{ id: 'HAR-102', file: 'img/Banner.png' },
{ id: 'HAR-103', file: '../private/key.txt' }
];
function inside(root: string, dest: string): boolean {
const rel = path.relative(root, dest);
return rel === '' || (!rel.startsWith('..' + path.sep) &&
rel !== '..' && !path.isAbsolute(rel));
}
moduleRoot使用path.resolve获得绝对基准,cases是输入清单而非执行结果。inside函数首先允许根目录本身,然后排除上跳与绝对相对结果。它只证明词法路径没有越出范围,尚未处理符号链接,所以还需要后续realpath检查。代码拒绝Windows驱动器前缀与反斜杠混用,避免跨平台时对路径分隔符产生不同理解;正式工具应另加输入长度上限和允许的文件扩展名白名单。
这一段没有使用任何未证实的ohpm内部API,所有行为都来自Node.js标准path和业务清单。模块名称third_har不必与HarmonyOS某个官方目录强绑定,团队可以按仓库规则调整;必须保证真正读取的路径与配置审核的路径来自同一份版本化清单。
三、大小写逐段比对不能靠文件是否存在
第二段代码是门禁关键:首先计算目标绝对路径并拒绝越界,然后从资源根目录开始,逐层读取目录条目,用完全相同的文件名匹配下一层。某一级找不到时,再尝试不区分大小写的比较,以此区别“确实不存在”和“只是拼写大小写不同”。即使运行在大小写不敏感的文件系统,也能由返回的目录真实名称识别这类问题。
async function inspectAsset(root: string, item: AssetRef): Promise<string> {
const raw = item.file;
if (!raw || path.isAbsolute(raw) || /^[A-Za-z]:/.test(raw) ||
raw.includes('\\')) return `${item.id} INVALID_PATH`;
const target = path.resolve(root, raw);
if (!inside(root, target)) return `${item.id} ESCAPE_ROOT`;
let cursor = root;
for (const part of path.relative(root, target).split(path.sep)) {
const entries = await fs.readdir(cursor);
if (!entries.includes(part)) {
const hit = entries.some((name: string) =>
name.toLowerCase() === part.toLowerCase());
return `${item.id} ${hit ? 'CASE_MISMATCH' : 'MISSING'}`;
}
cursor = path.join(cursor, part);
}
const realRoot = await fs.realpath(root);
const realTarget = await fs.realpath(cursor);
if (!inside(realRoot, realTarget)) return `${item.id} ESCAPE_ROOT`;
const stat = await fs.stat(realTarget);
return `${item.id} ${stat.isFile() ? 'PASS' : 'NOT_FILE'}`;
}
readdir每次返回当前目录项,因此这段示意更适合构建前的小规模清单,不适合几十万资源的高频扫描。大工程可以为每个父目录缓存目录列表,但缓存应以规范化后的真实路径为键,在一次校验结束后释放,避免后续文件改名却读取旧缓存。这里没有全局文件监听器;每次运行都重新读取,保证门禁不会把昨天的状态写成今天的结论。
路径越界存在第二层:即使字符串没有..,资源根目录中的某个子路径也可能是指向外部目录的符号链接。因此在逐级命中后,还要用realpath解析实际位置,再检查是否仍位于真实根目录之下。若根目录不存在、权限不足、文件被其他进程同时删除,fs会抛出异常,由外层统一转化为IO_ERROR。任何一种错误都应拒绝继续发布,而不是把异常当PASS。团队若允许符号链接,需要建立显式可访问根集合,不能简单删掉边界判断。
HAR-102的CASE_MISMATCH意味着当前目录中有banner.png但清单写Banner.png;如果两种拼写都真实存在,完整相等的名称优先通过,不应误报。HAR-103的ESCAPE_ROOT在进入文件系统遍历前就被阻止,即便外部private目录不存在也是这个结论,因为问题首先是违反访问范围,而不是路径有没有对应文件。

四、让CI结果能被人看懂,也能阻止错误出包
第三段代码将三条输入逐一送入检查器。这里刻意串行执行,便于日志顺序固定;若后续改为并发,仍需在归并结果时按输入ID排序,避免控制台顺序每次改变,影响评审对比。Node.js的process.exitCode不会突然杀死当前进程,能够让既定日志先写出,再由进程退出状态告知外部CI失败。失败退出码选择二只是团队自定义约定,并不是ohpm或者应用市场的规定。
async function runGate(): Promise<void> {
const lines: string[] = [];
for (const item of cases) {
try { lines.push(await inspectAsset(moduleRoot, item)); }
catch (err) {
const reason = err instanceof Error ? err.message : 'UNKNOWN';
lines.push(`${item.id} IO_ERROR (${reason})`);
}
}
const ok = lines.filter((line: string) => line.endsWith('PASS')).length;
console.log('PathGate / PathGate');
lines.forEach((line: string) => console.log(line));
console.log(`PASS ${ok}/${cases.length} ${ok === cases.length ? 'READY' : 'BLOCKED'}`);
if (ok !== cases.length) process.exitCode = 2;
}
runGate().catch((err: unknown) => { console.error(err); process.exitCode = 2; });
预期控制台包含HAR-101 PASS、HAR-102 CASE_MISMATCH、HAR-103 ESCAPE_ROOT,最后一行是PASS 1/3 BLOCKED。若脚本放在HAR发布前的本地检查步骤,建议把原始路径留给内部日志,提交到公开报告时仅保留脱敏后的相对引用;不能把项目私有绝对路径上传到第三方服务。
构造样例下,img/icon.png必须实际存在且为文件,img/banner.png也要实际存在才能产生CASE_MISMATCH。否则脚本会如实输出MISSING,这是有意设计,确保示例不靠杜撰检测结果自圆其说。对开发者而言,这种自解释规则比单一“失败”的报错更利于排查。团队还可以在CI中存储一份JSON结果,但应独立于最终HAR产物,避免调试报告无意进入生产包。

五、工程中的误报、安全和交付边界
这类门禁不是越严越好。路径中的大小写敏感度对目标部署环境确实重要,不过有些资源属于外部生成器产物,清单可能比生成步骤更早执行;如果生成顺序尚未确定,MISSING不能直接等同于坏包。解决方式是约定检查发生在资源生成之后、打包之前,并明确清单的来源版本。若某些文件是按产品变体选择加载,应该为每个变体分别运行,不把未启用变体的可选资源全部当成缺失。
还有符号链接、网络盘、权限和竞争条件。realpath检测只能证明检测时目标所在位置,不能保证下一毫秒文件没有改变。对高安全要求的供应链流程,还需要快照目录、锁定打包输入、产物校验和运行平台隔离;本例只作为发布前的辅助检查。路径里有Unicode同形字符、大小写折叠规则特殊的字符时,toLowerCase只是保守的诊断提示,并非完整Unicode安全审计。正式团队应记录实际文件名并确认是否需要NFC规范化,再与多平台文件系统策略对齐。
这篇文章的技术边界很清晰:Node.js标准fs/path在开发机上检查三方HAR资源清单,不涉及调用HarmonyOS设备端系统接口,也不自动修改包清单或签名。审核更广义的三方库质量仍要结合官方文档中的依赖、版本和授权要求,以及开发团队针对安全与许可证的独立流程。把路径约束提前做成一张可解释报告,是在正式构建开始之前降低沟通成本。
参考资料与验证边界
- 华为开发者:oh-package.json5 配置说明(2026-10-08更新):https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/ide-hmos-oh-package-json5
- 华为开发者:HAR共享包发布流程:https://developer.huawei.com/consumer/en/doc/harmonyos-guides/ide-har-publish
更多推荐



所有评论(0)