JS 层能力边界规范

状态:规范文档 运行时:JSH(JSVM),单 Env 关联设计:

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

  1. 设计 __flexui_bootstrap__ 对象,聚合 harmony template 真正需要的 4 个能力(NativeBackendContext、registerAdapters()、registerCard()、setupConsoleBridge())
  2. 将 __flexui_bootstrap__ 加入白名单,从白名单移除 __GLOBAL__、__flexui__、ConsoleModule
  3. 修改 harmony-entry-template.js,改为使用 __flexui_bootstrap__ 而非直接访问 __GLOBAL__ / __flexui__ / ConsoleModule
  4. 关闭 §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 发现),需确认影响面