JS 层能力边界规范
状态:规范文档 运行时:JSH(JSVM),单 Env 关联设计:
- js-native-call-entry-guard.md — 入口禁止机制
- js-native-call-timer-policy.md — 定时器策略管控
- js-native-call-origin.md — 调用来源追踪
1. 背景
flexui-engine 在同一个 JS VM(Hermes/JSH)中运行三层代码,同一 scope 内共享 globalThis。多 scope(FlexUIEngine.createScope)时每个 scope 拥有独立 Context/globalThis,保护化按 scope 各执行一次(见 §4.1):
| 层 | 加载时机 | 内容 |
|---|---|---|
| 引擎核心 | Scope 初始化 | flexui_jsfwk.js(lib/ + has.ts + packages) |
| 框架 JS | loadScopeBundles |
has_apis.js、agent_api.js、components.js |
| 业务 JS | loadCard / runScriptFromUri |
skill index.js、卡片 pack.js |
如果不加约束,业务代码可以访问引擎暴露在 globalThis 上的任何属性,包括 flexuiBridge(任意代码执行)、FlexUI.bridge(绕过 has.* 调任意 native module)、__GLOBAL__(篡改引擎状态)。
2. 核心原则:白名单制
globalThis的一级属性,白名单之外全部保护。业务 JS 只能访问白名单中的条目。
globalThis 一级属性全集
├── 白名单 → 保留原值,业务可访问
│ ├── has (public 对象)
│ ├── console (public 对象)
│ ├── global (全局对象自引用,防御性保留——见 §3.1)
│ ├── __consoleBusinessTag (业务日志 source tag)
│ ├── __GLOBAL__ (1期无脑加——pack.js bootstrap 读 appRegister;见 §8)
│ ├── __flexui__ (1期无脑加——pack.js bootstrap 读 NativeBackendContext/adapter)
│ ├── ConsoleModule (1期无脑加——pack.js bootstrap console bridge)
│ ├── setTimeout (policy 函数)
│ ├── clearTimeout (policy 函数)
│ ├── setInterval (policy 函数)
│ ├── clearInterval (policy 函数)
│ ├── requestAnimationFrame (policy 函数,豁免)
│ ├── cancelAnimationFrame (policy 函数,豁免)
│ ├── requestIdleCallback (policy 函数)
│ └── cancelIdleCallback (policy 函数)
│
└── 白名单之外 → 替换为抛错代理
├── flexuiBridge → throw Error ❌
├── dynamicLoad → throw Error ❌
├── FlexUI → throw Error ❌
├── __flexui_bridge__ → throw Error ❌
├── __flexui_env → throw Error ❌
└── ... 其他一律保护
为什么只需要一级:一级属性替换后,其下的二级/三级属性自然不可达。例如 FlexUI 被替换为代理,FlexUI.bridge.callNative 路径第一步就抛错,不需要递归处理。
为什么不区分函数/对象:对象替换和函数替换同理——globalThis.xxx = proxy,闭包内提前 capture 的旧引用不受影响。没有例外。
ReadOnly 残余通道(必须显式认知):flexuiCallNatives 由 C++ 以 PropertyAttribute::ReadOnly 创建(js_driver_utils.cc RegisterCallHostObject),protect() 无法替换——业务 JS 可直接 flexuiCallNatives(module, method, ...) 调用任意 native 模块,绕过本清单。该通道的兜底在 js-native-call-origin.md:第 7 参缺失 → origin='unknown' → ETS fail-closed 拒绝敏感接口。入口禁止不是独立闭环:目标安全水位 = 入口禁止 + 归因层同时上线(见整改计划 §4)。
为什么安全:Object.keys(globalThis) 只枚举可枚举属性。JS 内置对象(Object、Array、Promise、Map、Date、JSON、Math、parseInt 等)都是 enumerable: false,不会被枚举到,不会被替换。实际被保护的都是引擎通过 globalThis.xxx = ... 显式添加的可枚举属性。Webpack 编译的业务 bundle 是 IIFE 包裹,引用内置对象不受影响。
例外(显式处理):
eval与Function构造器虽是enumerable: false内置属性(枚举循环覆盖不到),但属于动态代码执行入口,在 protect() 时显式替换为抛错代理(连同Function.prototype.constructor及 async/generator 构造器),业务 JS 不可eval/new Function执行任意代码字符串——见 js-native-call-entry-guard.md §3.5。框架自身在 protect() 前加载、运行期不使用 eval/new Function,不受影响。
3. 白名单
3.1 public(业务直接使用)
| 条目 | globalThis 位置 | 提供方 | 说明 |
|---|---|---|---|
has |
globalThis.has |
has_apis.js | 100+ 小程序 API(storage/network/media/file/BLE/...) |
console |
globalThis.console |
ConsoleModule.js | 业务日志,自动带 [businessConsoleTag] |
global |
全局对象自引用 | JSVM/Hermes | 防御性保留:全库(js2native.js 等)运行时经 global.xxx 解析(如 typeof global.flexuiCallNatives),若被替换整个框架的 callNative 链路中断。global 即 globalThis 本身,业务可访问无安全增益 |
__consoleBusinessTag |
globalThis.__consoleBusinessTag |
has.ts(flexui-backend) | 业务 console 的 source tag(纯字符串)。console 实现每次调用运行时读取(ConsoleModule.js),而该值在保护化前写入——若被替换为代理,console.log 会把函数对象当 tag 传给 C++。必须保留原值 |
3.1.5 pack.js bootstrap 依赖(harmony-entry-template 生成代码使用)
以下条目被 harmony-entry-template.js 生成的 pack.js bootstrap 代码在保护化后直接访问(只读框架构建工具/日志桥),不提供代码执行能力,不构成越权。加入白名单以保障 pack.js 正常加载。
| 条目 | globalThis 位置 | 模板行 | 用途 | 风险评估 |
|---|---|---|---|---|
__GLOBAL__ |
globalThis.__GLOBAL__ |
:100 | 读 appRegister 注册组件 |
🟢 appRegister 本身只是组件名→入口函数的映射表,不提供代码执行 |
__flexui__ |
globalThis.__flexui__ |
:57, :92-96, :109 | 读 NativeBackendContext/has/adapter 等框架构建工具;写 codeSpace/initWithBackend 注入 skill 数据 |
🟢 这些是创建 backend、注册 adapter 的构建工具,不直接提供代码执行能力 |
ConsoleModule |
globalThis.ConsoleModule |
:29 | 增强 console.* → hilog 路由(catch 包裹,失败静默跳过) |
🟢 纯日志桥接,无代码执行面 |
注意:
globalThis.__ascfCallbacks不在白名单——agent_api.js 不应将其暴露到 globalThis(与jsModuleList['__ascfCallbacks']是同一个真身对象,暴露即后门)。整改计划中要求删除 agent_api.js 的globalThis.__ascfCallbacks = ascfCallbacks行(harmony template 只做typeof检查,删除后返回'undefined'无功能影响)。 |
业务面条目审计(Phase 0):
fetch/Headers/Response(Network.js)、localStorage/localStorageAsync(Storage.js)、turboPromise(flexui.js)等挂载在 globalThis 的 API 目前不在白名单。2026-08 初查业务代码(demos/、ascf-ai-demo、pack.js)未见直接引用(网络走has.request),但任何业务代码引用它们前必须先评审加白名单。__ascfInstanceId__不需要白名单:业务入口在保护化后赋值(覆盖代理,见 §4.1)。
3.2 policy(经 ETS 策略检查后可用)
| 条目 | globalThis 位置 | 提供方 | 当前状态 |
|---|---|---|---|
setTimeout |
globalThis.setTimeout |
TimerModule.js | 后续经 TimerPolicyModule 策略检查 + C++ 定时 |
clearTimeout |
globalThis.clearTimeout |
TimerModule.js | 同上 |
setInterval |
globalThis.setInterval |
TimerModule.js | 同上 |
clearInterval |
globalThis.clearInterval |
TimerModule.js | 同上 |
requestAnimationFrame |
globalThis.requestAnimationFrame |
AnimationFrameModule.js | 豁免(高频渲染路径,保留 internalBinding 直连) |
cancelAnimationFrame |
globalThis.cancelAnimationFrame |
AnimationFrameModule.js | 同上 |
requestIdleCallback |
globalThis.requestIdleCallback |
TimerModule.js | 后续经 TimerPolicyModule |
cancelIdleCallback |
globalThis.cancelIdleCallback |
TimerModule.js | 同上 |
4. 保护机制
4.1 保护化流程
protect():
for each key in Object.keys(globalThis):
if key in WHITELIST → skip
globalThis[key] = throwProxy(key)
就四行。白名单之外的 globalThis 一级属性全部替换为抛错代理。
要点(review 补充):
- 按 scope 执行:每 scope 独立 globalThis,
protect()在该 scope 的附加 bundle 全部加载完成后调用一次(见整改计划 2.6);__flexuiProtected标记为 per-globalThis。 - 保护化后的赋值覆盖:宿主/ETS 经
callJavaScriptModule向 globalThis 写入新属性(如业务入口globalThis.__ascfInstanceId__ = instId)是直接赋值覆盖代理,安全。业务入口加载晚于保护化,此模式成立。 - 真身捕获必须在保护化之前:所有需要在保护化后被回调的框架代码(§5),必须在其加载时(IIFE/require 阶段)capture 所需全局。
- ReadOnly 属性(如
flexuiCallNatives)赋值失败/被跳过——见 §2 残余通道。
4.2 真身通道
保护化后有两类代码需要访问被保护的属性,它们不通过 globalThis 走,不受影响:
引擎核心 bundle 代码(lib/*.js):在 core bundle require 阶段执行,早于保护化。需要被 C++/ETS 在保护化后回调的函数,在加载时 capture 所需引用到闭包。
// native2js.js — flexuiBridge 会被 ETS→JS 路径在保护化后调用
var _GLOBAL = __GLOBAL__; // 加载时 capture,保护化后 globalThis.__GLOBAL__ 是代理
global.flexuiBridge = function(action, callObj) {
// 使用 _GLOBAL,不用 globalThis.__GLOBAL__
var targetModule = _GLOBAL.jsModuleList[callObj.moduleName];
};
外挂框架 JS(has_apis.js、agent_api.js):在 IIFE 执行时 capture,早于保护化。
// agent_api.js
var _bridge = globalThis.FlexUI.bridge; // IIFE capture
// 保护化后 globalThis.FlexUI 是代理,但 _bridge 指向真身
C++→JS:C++ eager capture flexuiBridge 真身引用,不经过 globalThis 查找。
4.3 保护化后的调用路由
业务 JS:
flexuiBridge(...) → globalThis.flexuiBridge 是代理 → throw ❌
__GLOBAL__.jsModuleList → globalThis.__GLOBAL__ 是代理 → throw ❌
FlexUI.bridge → globalThis.FlexUI 是代理 → throw ❌
has.getSystemInfo() → globalThis.has 在白名单 → 正常 ✓
ETS→JS:
callJavaScriptModule → C++ scope->GetBridgeObject() (eager capture 真身)
→ native2js.js 闭包 _GLOBAL.jsModuleList (capture 真身)
→ 正常 ✓
框架 JS:
agent_api callNative() → IIFE capture _bridge.callNative (真身) → 正常 ✓
has_apis has.* → IIFE capture callNative (真身) → 正常 ✓
5. 框架 JS 编写规则
5.1 核心原则
「加载时 capture,不运行时 lookup」
框架 JS 在引擎初始化期间执行(保护化之前),此时 globalThis 上都是真身。保护化后白名单外的一级属性全被替换。因此框架 JS 必须在加载时 capture 所需引用到闭包中。
capture 的对象不止 __GLOBAL__/FlexUI:ConsoleModule、__flexui_bridge__ 等一切运行时还会用到的受保护全局都要 capture(§5.2/§5.3 清单)。纪律:保护化之后,框架代码运行时不得解析任何 globalThis 一级标识符(typeof global.xxx 判断、globalThis.FlexUI && ... 链都在禁止之列——结果可能已被替换为代理)。
// ❌ 运行时查找 → 保护化后读到代理
function callNative(method, params) {
globalThis.FlexUI.bridge.callNative('AscfAIModule', method, params)
}
// ✅ 加载时 capture → 闭包保存真身
var _bridge = globalThis.FlexUI.bridge
function callNative(method, params) {
_bridge.callNative('AscfAIModule', method, params)
}
5.2 引擎核心 bundle(lib/*.js)需 capture 的全局
以下 engine 函数会被 C++/ETS 在保护化后以非 globalThis 路径调用,但其内部实现需要访问已被保护的全局对象。这些文件需要在加载时 capture:
| 文件 | 需 capture | 原因 |
|---|---|---|
native2js.js |
__GLOBAL__ |
callJsModule 需要 __GLOBAL__.jsModuleList |
Event.js |
__GLOBAL__, FlexUI |
__loadInstance__ 需要 __GLOBAL__.appRegister、__GLOBAL__.jsModuleList.EventDispatcher |
js2native.js |
__GLOBAL__, flexuiCallNatives |
callNative 需要 __GLOBAL__.moduleCallList、flexuiCallNatives |
AnimationFrameModule.js |
__GLOBAL__ |
rAF 回调需要 __GLOBAL__ |
ConsoleModule.js |
—(内部自用) | reportUncaughtException 直接用闭包内 consoleModule.Log(internalBinding),不读 globalThis.ConsoleModule(该标识符保护化后为代理) |
5.3 外挂框架 JS 编写规则
| # | 规则 | ✅ | ❌ |
|---|---|---|---|
| 1 | 日志 | IIFE 顶部 var _consoleModule = ConsoleModule,运行时用 _consoleModule.log/warn/error |
运行时解析 globalThis.ConsoleModule.*(保护化后是代理,直接抛错);console.*(业务面) |
| 2 | capture | IIFE 顶部 var _b = FlexUI.bridge,后续用 _b |
函数内运行时查找 |
| 3 | 公开 API | 挂到 globalThis.has.xxx |
直接挂 globalThis.xxx |
| 4 | moduleName | 固定自己的命名空间(如 'AscfAIModule') |
接受外部传入 |
| 5 | 幂等 | IIFE 顶部检查 __<name>Loaded,已加载则 return |
无幂等守卫 |
| 6 | 依赖校验 | 加载时检查必需依赖,缺则 ConsoleModule.error + return |
假设一定存在 |
| 7 | 事件监听 | IIFE 顶部 capture __bridge(has_apis.js 已捕获),回调内用 __bridge.callNative(...) |
回调内 globalThis.FlexUI && globalThis.FlexUI.bridge...(保护化后 FlexUI 是代理,&& 链恒 false,socket/BLE/WiFi 事件回调静默失效) |
| 8 | 业务可读全局 | 不改写白名单条目(has/console/global 等) |
覆盖 globalThis.has 等业务入口 |
不可用(外挂框架 JS 通过 runScriptFromUri 加载,不在 bootstrap 闭包内):internalBinding、NativeModule.require、flexuiCallNatives(应通过 FlexUI.bridge.* 间接使用)。
5.4 agent_api.js 改造对照
// Before (❌ 运行时查找):
function getBridge() {
return globalThis.FlexUI && globalThis.FlexUI.bridge
}
// After (✅ 加载时 capture):
var _bridge = globalThis.FlexUI && globalThis.FlexUI.bridge
agent_api.js 需要 capture 的其余受保护全局(整改计划 2.5 有完整清单):
__flexui_bridge__—invokeAsyncCallback运行时读取 callbackManager(agent_api.js:466)ConsoleModule—executeSkillApi/invokeAsyncCallback运行时日志(agent_api.js:373-415、462-471)__ascfInstanceId__不需要 capture:该值由业务入口在保护化后写入(赋值覆盖代理),运行时读取拿到真值
6. 集成方白名单配置
白名单是引擎的静态配置,在引擎创建时传入,经 C++ __FLEXUINATIVEGLOBAL__ 送达 JS(JS 运行前已就绪)。entry_guard.js 在 core bundle require 阶段读取。
6.1 默认值
DEFAULT_WHITELIST = [
'has', 'console', 'global', '__consoleBusinessTag',
'setTimeout', 'clearTimeout', 'setInterval', 'clearInterval',
'requestAnimationFrame', 'cancelAnimationFrame',
'requestIdleCallback', 'cancelIdleCallback',
];
global防御性保留(全局对象自引用,业务可访问无安全增益);__consoleBusinessTag为 console 实现运行时读取的业务 tag。fetch/Headers/Response、localStorage/localStorageAsync、turboPromise等候选条目以 Phase 0 审计(整改计划)终稿为准——当前业务代码未见直用,默认保护 + 评审放行。
6.2 集成方追加
FlexUIEngine.createEngine({
entryGuard: {
allowGlobalNames: ['myCustomApi'], // 追加到白名单
},
})
配置经 EngineInitParams → C++ global_config → __FLEXUINATIVEGLOBAL__.entryGuardAllowGlobal → entry_guard.js 读取合并。
7. 新增能力默认规则
所有新增 globalThis 一级属性默认不在白名单中,自动被保护化拦截。确认可公开 → 审批 → 加入白名单。
8. 已知缺口与后续计划
8.1 1 期已知缺口(__GLOBAL__ / __flexui__ / ConsoleModule 无脑加入白名单的副作用)
__GLOBAL__、__flexui__、ConsoleModule 为兼容 pack.js bootstrap(harmony-entry-template.js 生成代码)而加入白名单,业务代码同样可以访问它们下面的所有属性和方法,存在以下已知旁路:
| 旁路路径 | 危害 | 2 期修复方向 |
|---|---|---|
__GLOBAL__.jsModuleList.__ascfCallbacks.executeSkillApi(...) |
任意代码执行 | 2 期不再暴露 __GLOBAL__,改为 bootstrap API |
__GLOBAL__.jsModuleList.__flexui_env.updateEnv(...) |
环境篡改 | 同上 |
__flexui__.callNative('ASAPIsModule', ...) |
绕过 has.* 调任意 native module |
同上 |
__flexui__.callNativeSync(...) |
同上 | 同上 |
__flexui__.globalOptions / registerBehavior / registerElement / triggerRender |
框架级能力滥用 | 同上 |
__GLOBAL__.moduleCallList |
篡改 native 回调注册表 | 同上 |
ConsoleModule.log/warn/error(...) |
框架日志面暴露(tag 固定 [FlexUI],非业务 tag) |
低风险,2 期一并收敛 |
8.2 2 期方案:最小 bootstrap API
- 设计
__flexui_bootstrap__对象,聚合 harmony template 真正需要的 4 个能力(NativeBackendContext、registerAdapters()、registerCard()、setupConsoleBridge()) - 将
__flexui_bootstrap__加入白名单,从白名单移除__GLOBAL__、__flexui__、ConsoleModule - 修改
harmony-entry-template.js,改为使用__flexui_bootstrap__而非直接访问__GLOBAL__/__flexui__/ConsoleModule - 关闭 §8.1 全部旁路
8.3 1 期额外修复项
| 修复项 | 说明 |
|---|---|
删除 agent_api.js 的 globalThis.__ascfCallbacks = ascfCallbacks |
与 jsModuleList['__ascfCallbacks'] 是同一真身对象,暴露即后门。harmony template 只做 typeof 检查,删除后返回 'undefined' 无功能影响 |
审查 file-generator.js 生成的 globalThis.require = AscfApi.require |
覆盖 require 语义的全局写(agent report 发现),需确认影响面 |