首页
/ Insomnia 模板标签 QuickJS 沙箱:用官方示例插件验证沙箱隔离、模块白名单与权限门控

Insomnia 模板标签 QuickJS 沙箱:用官方示例插件验证沙箱隔离、模块白名单与权限门控

2026-09-05 20:55:53作者:吴年前Myrtle

本文以仓库自带的 insomnia-plugin-sandbox-demo 示例插件为主线,完整讲解 Insomnia 模板标签 QuickJS 沙箱的验证方法:如何在开发模式下安装并切换"Run template tags in sandbox"开关、如何使用 6 个探针标签逐一验证沙箱运行环境、宿主桥接(host bridge)、清单门控的模块注册表(M1/M2/M3)、宿主能力授权(C1)与多文件插件(M4);并结合 模块注册表沙箱引导脚本宿主桥接实现 等源码,说明这些行为背后的实际隔离机制与报错契约,便于你在开发或审查社区插件时准确判断其运行边界。

这个示例插件是干什么的

examples/insomnia-plugin-sandbox-demo 是一个手工测试夹具(manual test fixture),用于验证"QuickJS 模板标签沙箱"(Milestone 2)这条执行路径。它由三部分构成:

  • index.js:导出 templateTags 数组,包含 6 个探针标签,每个标签专门验证沙箱的一个维度;
  • package.json:声明了 insomnia.permissions 权限清单,是清单门控(manifest gating)的活体样本;
  • lib/greeting.js:被 index.js 以相对路径 require('./lib/greeting') 引入的兄弟模块,用于验证多文件插件(M4)。

README 的定位说明:sandboxprobe 标签会报告自己执行在哪里,并演练一次异步宿主桥接往返。沙箱开关关闭时输出 hello | ran in: main-process | arch via bridge: <arch>;开启时输出 hello | ran in: sandbox | arch via bridge: <arch>

安装与运行(开发模式)

README 中的步骤:

  1. 在仓库根目录运行应用:npm run dev
  2. 打开 Preferences → Plugins,点击 Reveal Plugins Folder
  3. examples/insomnia-plugin-sandbox-demo 整个文件夹复制到该插件目录;
  4. 点击 Reload Plugins
  5. Preferences → Scripting 中切换 Run template tags in sandbox (experimental) 开关;
  6. 在某个请求的 URL 或 Header 中插入 Sandbox Probe 模板标签(或直接输入 {% sandboxprobe 'hi' %}),观察预览结果随开关切换而变化。

从源码结构看,这个"experimental"开关对应 settings 定义 中的 pluginSandboxEnabled 字段,其注释明确写着"T1:把所有不可信(用户)插件面——模板标签、request/response 钩子、actions 以及加载期模块代码——全部放进 QuickJS-WASM 沙箱,用户插件默认拒绝(default-deny)"。也就是说该开关影响的不仅是模板标签,而是一整套用户插件执行面。

逐标签验证:6 个探针各测什么

sandboxprobe:运行位置 + 异步宿主桥接

sandboxprobe 标签 做了两件事:

// 判定运行环境:沙箱注入 INSOMNIA_TEMPLATE_SANDBOX 标记,主进程旧路径没有
const ranIn = typeof INSOMNIA_TEMPLATE_SANDBOX !== 'undefined' ? 'sandbox' : 'main-process';

// 演练一次异步宿主桥接:context.util.nodeOS -> pluginToMainAPI['nodeOS']
let arch = 'n/a';
try {
  const os = await context.util.nodeOS();
  arch = os.arch;
} catch (err) {
  arch = 'bridge-error:' + err.message;
}
return `${label} | ran in: ${ranIn} | arch via bridge: ${arch}`;

两个值得注意的细节,README 都特意解释了:

  1. 为什么用专用标记而不是 process 来区分环境:早期用 typeof process 判断即可,但从 M2 开始沙箱也提供了一个 process 桩(stub),两者都"有 process",因此需要专门的标记。源码印证了这一点——in-sandbox-bootstrap.ts 中以 writable: false, configurable: false 定义该全局量,插件代码无法伪造:

    Object.defineProperty(globalThis, "INSOMNIA_TEMPLATE_SANDBOX",
      { value: true, writable: false, configurable: false, enumerable: true });
    
  2. arch via bridge 证明异步往返链路完好:标签内 await context.util.nodeOS() 走的是 __hostBridge('nodeOS', ...)。从 in-sandbox-bootstrap.ts 看,沙箱捕获宿主注入的 __hostBridge 函数后立即从 globalThis 删除(防止插件代码绕过 context.* 直接调用原始桥接),所有异步能力都收敛到唯一的 __bridge(path, body) 辅助函数;宿主侧则由 host-bridge.ts 按注入点(renderer / main / CLI)分派到真实的 pluginToMainAPI 处理器,结果以 JSON 字符串回传并恢复。另外 quickjs-runtime.ts 的注释解释了为何使用同步版 QuickJS WASM:异步化(asyncified)变体在用户代码 await 宿主调用时会损坏,因此宿主桥接返回 VM 原生 Promise,由编排器泵 executePendingJobs() 来结算。

requireprobe:清单门控的模块解析(M1)

requireprobe 标签 演示 require() 的两段式默认拒绝:

  • {% requireprobe 'path' %} → 输出 a/b(基线授权 + 精选注册表);
  • {% requireprobe 'fs' %} 或任意 npm 包 → 报错 Module 'X' not permitted by manifest
  • 已授权但 Insomnia 未打包的模块 → 报错 Module 'X' not available in sandbox

源码中这两条错误信息是用户可见契约,被 in-sandbox-bootstrap.ts 的 __require 逐字抛出,并附带稳定判别码(SANDBOX_MODULE_NOT_PERMITTED / SANDBOX_MODULE_NOT_AVAILABLE)供宿主分支处理,单测与冒烟测试对文案逐字断言。解析逻辑与 module-registry.ts 文件头注释 一致:先查授权集(未授权→not permitted),再查注册表工厂(无工厂→not available)。当前注册表收录的模块为 pathcrypto(基线,无需声明)、events(需声明),以及两个重量级(heavy: true、仅在插件声明时才打包进沙箱源码以避免每次渲染解析数百 KB)的官方捆绑 npm 库 uuidajv

README 还说明了路线图:注册表覆盖范围在 M2/M3 逐步扩大;相对文件 require('./util') 随插件预打包(M4)提供——而本示例的 multifileprobe 标签已实际验证了这一点。

eventsprobe:清单声明式模块授权(C3)

eventsprobe 标签 演示"声明了就能用":本插件 package.json 声明了 insomnia.permissions.modules: ["events"],因此标签内可以:

const EventEmitter = require('events').EventEmitter;
const emitter = new EventEmitter();
let out = '';
emitter.on('ping', value => { out = value; });
emitter.emit('ping', 'events-ok');
return out; // 渲染为 events-ok

若某个插件未声明 events,同样一行 require('events') 就会得到 Module 'events' not permitted by manifest。从源码看,沙箱里的 events 并非 Node 原生内置,而是 module-registry.ts 中一段纯 JS 的极简 EventEmitter 重实现(覆盖 on/once/emit/removeListener 等常用面)——这正是"vetted safe equivalent"原则:require 永远解析到 Insomnia 提供的安全等价物,绝不是原始 Node 内置

README 还提示:Preferences → Plugins 界面会显示每个插件已声明的权限(本插件显示 modules: events,以及下文的 storage)。

stdlibprobe:环境内置沙箱全局(M2)

stdlibprobe 标签 演示无门控、始终存在的沙箱内置全局:BufferURL/URLSearchParams、冻结的 process 桩、以及 Web-Crypto 的 crypto.getRandomValues/crypto.subtle。它们是纯 JS 或宿主支撑的安全等价物,渲染结果与旧的主进程路径一致:

  • {% stdlibprobe 'buffer' %}Buffer.from('hi 👋').toString('base64')
  • {% stdlibprobe 'url' %}new URL('https://example.com:8443/p?a=1').host
  • {% stdlibprobe 'platform' %}process.platform

从源码结构看,除这些全局外,沙箱引导脚本还在最前面补齐了 QuickJS 缺失的 Web API(btoa/atob/TextEncoder/TextDecoder)与转发到宿主的 console(见 in-sandbox-bootstrap.ts)——这些同样不属于清单门控范围。

capabilityprobe:清单门控的宿主能力(C1)

capabilityprobe 标签 演示第二道授权轴——宿主能力:本插件声明了 insomnia.permissions.capabilities: ["storage"],因此可以完成 context.store 的 set/get 往返:

await context.store.setItem('demo_k', 'storage-ok');
return await context.store.getItem('demo_k'); // 渲染为 storage-ok

未声明 storage 的插件会收到 Capability 'storage' not granted — add it to insomnia.permissions.capabilities。基线能力(rendermodels.readutilcrypto)无需声明;network / storage / fs-read / app 必须声明;credentials 为第一方捆绑插件保留,社区插件即使声明也无法获得(它高于模板标签面的能力上限,详见 PERMISSIONS.md)。

从实现看,能力门控是双层防御

  1. 宿主侧桥接门(C1)host-bridge.ts 的 filterByCapabilities 包装每个桥接路径的处理器,路径未映射到任何能力→拒绝调用(fail-closed);映射的能力未被授权→抛出上述 "not granted" 错误。BRIDGE_PATH_CAPABILITIES 是"每条沙箱可调用桥接路径 → 能力"的唯一映射,且有完整性单测保证引导脚本新增桥接路径而此处漏配时测试直接失败。
  2. 沙箱内上下文形状门(C2)__buildContext 先构建完整 context,再删除未授权分支(如 if (!__hasCap("storage")) { delete ctx.store; })。因此未授权分支是 undefined 而非"存在但会拒绝的桩"——插件可以用 if (context.network) 做特性探测并优雅降级,且触碰未授权分支会在插件自己的属性访问处同步失败,而不是之后的异步 rejection。

vendoredprobe:官方审定的 npm 库(M3)

README 未单列的第六类探针 vendoredprobe 演示第三方库路径:本插件声明 modules: ["uuid"],标签内 require('uuid') 得到的是 Insomnia 精确钉版并预打包的库副本,执行 uuid.validate(uuid.v4()) 后渲染 uuid=ok / uuid=bad

PERMISSIONS.md 对此有严格的供应链说明:每个 vetted 库来自隔离的、精确钉版的安装(src/templating/sandbox/vendored/pkg/),不是应用自身可能更新版本的 uuid/ajv 依赖,因此沙箱版本可独立审查且绝不随例行依赖升级漂移;不变量是"沙箱钉版永不领先应用自身解析版本",由 npm run sandbox:vendored:guardrail -w insomnia(CI 亦运行)强制。新增库通过 scripts/generate-sandbox-vendored.ts 谨慎添加;升级已审定库则优先使用 npm run sandbox:vendored:upgrade -w insomnia -- <lib>@<version>(它会校验安装版本与重新生成包内版本一致、检查上述护栏并先跑该库的回归测试)。

另外 PERMISSIONS.md 特别警告:run() 内 require vetted 库,而不是顶层。Insomnia 发现插件标签时仍在宿主进程中加载入口文件,顶层 require('uuid')/require('ajv') 在那里无法解析(它们只存在于沙箱注册表,不在插件旁边的磁盘上);相对 require(如 ./util)则可以在顶层使用。

multifileprobe:多文件插件(M4)

multifileprobe 标签 配合顶层 const greeting = require('./lib/greeting') 演示多文件插件:沙箱把插件自己的源文件读入模块映射,在沙箱内解析相对 require;插件的 node_modules 永不被参考。lib/greeting.js 的注释强调:"a bare require('uuid') 总是解析到 vetted 注册表副本,所以插件无法为已授权模块提供替换实现"("poison" 保证)。

实现上,相对模块加载器 从宿主打入的 __moduleFiles 映射解析 ./x../x./lib(→ ./lib/index.js)等候选,源文件以 envelope 数据(而非宿主执行的代码)身份传入,用 new Function 在沙箱内编译;路径上溯超出插件根目录(如 require('../../util'))直接判为不可解析,防止"越界路径伪装成插件内模块"。宿主读取插件源码时只加载插件目录内的 .js/.json 文件,跳过 node_modules 和点目录。

权限清单:完整参考

示例插件的 package.json 是最小活样本:

{
  "name": "insomnia-plugin-sandbox-demo",
  "main": "index.js",
  "insomnia": {
    "permissions": {
      "modules": ["events", "uuid"],      // require(x) 允许返回什么
      "capabilities": ["storage"]         // context.* 允许调用的宿主桥接组
    }
  }
}

PERMISSIONS.md 给出的能力轴全表如下(✅ = 基线,无需声明):

Capability 授权内容 基线?
render context.util.render
models.read 只读 context.util.models.* 查询
util context.util.nodeOS / decode / encode
crypto require('crypto') 宿主函数
network context.network.* 需声明
storage context.store.* 需声明
fs-read context.util.readFile 需声明
app context.app.*context.util.openInBrowser 需声明

模块轴的对应关系(与 module-registry.ts 的注册表一致):

  • 基线(无需清单)pathcryptoTEMPLATE_TAG_BASELINE_MODULES);
  • 可授权:注册表内其余模块——纯 JS 重实现(如 events)与官方钉版捆绑库(uuidajv);
  • 错误契约:未授权 → Module 'X' not permitted by manifest;已授权但未打包 → Module 'X' not available in sandbox

迁移行为:未声明任何 permissions 块的插件按基线授权运行;若随后触达非基线模块,会收到一次性迁移通知,明确指出应添加的授权名——添加 insomnia.permissions 块并重新加载插件即可。

安全设计要点(源码级总结)

结合本示例覆盖到的源码,沙箱的关键隔离机制可以归纳为:

  1. 运行位置可特征检测但不可伪造INSOMNIA_TEMPLATE_SANDBOX 标记为非可写、非可配置的全局(in-sandbox-bootstrap.ts)。
  2. 默认拒绝的双轴清单:模块轴(require 只能命中"授权 ∩ 注册表")与能力轴(context.* 分支缺失 + 宿主桥接拒调),两条轴相互独立、分别声明。
  3. 安全等价物而非原始内置:所有注册表模块工厂是仓库内受信字面量(module-registry.ts 明确标注其为代码注入风险点,registerCall 还会对非函数表达式工厂直接抛错拒绝),重 JS 只来自"纯 JS 重实现"或"宿主支撑 shim"。
  4. 信封数据捕获后即删除:宿主打入的 __envelopeJSON/__tagName/__hostBridge 在引导脚本开头被捕获进闭包并从 globalThis 删除,插件顶层代码无法篡改授权集或直接调用原始桥接(in-sandbox-bootstrap.ts)。
  5. 多文件加载的"毒化"保证:插件自带 node_modules 永不参与解析,裸模块名只能命中注册表副本(in-sandbox-bootstrap.ts)。
  6. 沙箱内部入口点锁定__require__invoke__loadPluginEntry 等在插件代码运行前被 Object.defineProperty 冻结,插件无法替换自身的门控(in-sandbox-bootstrap.ts)。

适用前提与限制

  • 本示例是手工验证夹具,不是自动化测试套件;它依赖 npm run dev 的开发环境,以及 Preferences 里的实验开关(README 原文即标注 experimental)。
  • 验证输出中的 arch 具体值取决于你运行 Insomnia 的机器架构;sandboxprobe 在开关切换时 ran in 的翻转(main-processsandbox)才是核心观察点。
  • 能力/模块的具体取值以当前仓库的 PERMISSIONS.mdmodule-registry.ts 为准——README 中提到的 M1–M4 里程碑对应注册表覆盖逐步扩大的过程,例如相对 require('./util') 依赖 M4 的多文件支持,而 ajv 等 vetted 库的加入属于 M3。
登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.13 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
529
593
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
915
1.83 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.58 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.35 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
515
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
388