Hoppscotch JavaScript Sandbox:为 Pre-Request 与 Test 脚本构建安全隔离执行环境的实现解析 Hoppscotch JavaScript Sandbox为 Pre-Request 与 Test 脚本构建安全隔离执行环境的实现解析【免费下载链接】hoppscotchOpen-Source API Development Ecosystem • https://hoppscotch.io • Offline, On-Prem Cloud • Web, Desktop CLI • Open-Source Alternative to Postman, Insomnia项目地址: https://gitcode.com/GitHub_Trending/ho/hoppscotchpackages/hoppscotch-js-sandbox是 Hoppscotch 中专门负责在受控隔离环境中执行用户编写的 JavaScript 脚本的包覆盖 Pre-Request 脚本发送请求前改写请求与 Post-Request/Test 脚本请求返回后断言响应两大安全敏感场景。本文基于 该包 README 的骨架结合包内源码逐项拆解它的双执行后端架构、公开 API、脚本级联合并机制与错误恢复策略帮助读者理解一个跨 Web/Node 的 JS 沙箱是如何在“能力可用”与“宿主安全”之间取得平衡的。一、这个包解决什么问题README 对该包的定义非常直接This package deals with providing a JavaScript sandbox for executing various security sensitive external scripts.Hoppscotch 的集合Collection中允许用户为每个请求编写自定义脚本Pre-Request 脚本可以在发请求前修改 URL、Header、Body 和环境变量Test 脚本可以在拿到响应后做断言。这些脚本本质上是不受信任的外部代码——它们运行在用户浏览器或 CLI 进程中却能触达环境变量可能含密钥、Cookie、请求对象等敏感数据。如果直接eval一段恶意或出错的脚本就可能污染宿主环境因此需要一个真正隔离的执行沙箱。README 说明该包基于 faraday-cage 底层的 QuickJSWASM 编译的 JavaScript 引擎 构建隔离环境README 原文提到的 quickjs-emscripten 即 QuickJS 的 Emscripten/WASM 构建形态并明确当前实现的沙箱有两类Hoppscotch Test ScriptsPost-Request 测试脚本Hoppscotch Pre Request ScriptsPre-Request 预请求脚本同时 README 标注该包处于ALPHA阶段遵循语义化版本但在 1.0 之前“代码与公开 API 不应被视为已固定和稳定”。二、包结构与三套入口从 package.json 可以看到包名hoppscotch/js-sandbox当前版本 2.1.0它通过exports字段提供三条独立的子路径入口入口构建产物适用宿主.类型dist/types/index.d.ts类型引用./webdist/web.js/dist/web.cjs浏览器Web 版、桌面端 WebView./nodedist/node.js/dist/node.cjsNode.js如 hoppscotch-cli./scriptingdist/scripting.js/dist/scripting.cjs纯字符串工具函数构建配置 用 Vite 库模式同时编译三个入口web: ./src/web/index.ts、node: ./src/node/index.ts、scripting: ./scripting.ts分别产出 ESM 与 CJS 双格式。三条入口对应 src/web/index.ts、src/node/index.ts、src/scripting.ts。其中./scripting子路径单独抽离 src/utils/scripting.ts 中的字符串工具combineScriptsWithIIFE、stripModulePrefix、filterValidScripts等让消费方可以不加载任何 worker/沙箱运行时就完成脚本合并——这是为 CLI 等“只需字符串处理、执行另有安排”的场景准备的。关键依赖也能从依赖表读出设计取向faraday-cage0.1.0QuickJS 沙箱宿主实验性后端的执行核心fp-tsTaskEither/Either函数式错误处理所有公开 API 都以“错误字符串 | 成功结果”的 Either 语义返回而不是抛异常acorn在沙箱启动前对脚本做宿主侧语法预解析chai6.2.2沙箱内expect()断言的底层isolated-vmpeerDependency可选6.1.2Node 侧 legacy 后端的 V8 隔离区方案。engines要求node 22。此外postinstall与prepublish钩子都会执行pnpm run build意味着作为 workspace 包被安装后会自动构建产物。三、双执行后端experimental 与 legacy包内最核心的架构特征是同一套公开 API 之下藏着两套执行后端由调用方通过选项区分。以 Web 端 Pre-Request 为例src/web/pre-request/index.ts 中的分发逻辑是export const runPreRequestScript ( preRequestScript: string, options: RunPreRequestScriptOptions ): PromiseE.Eitherstring, SandboxPreRequestResult { const { envs, experimentalScriptingSandbox true } options if (experimentalScriptingSandbox) { // 走 faraday-cageQuickJS/WASM return runPreRequestScriptWithFaradayCage(...) } // legacyWeb Worker new Function return runPreRequestScriptWithWebWorker(preRequestScript, envs) }注意experimentalScriptingSandbox默认为true即默认走新后端。两个后端的特点1. experimentalfaraday-cageQuickJS/WASMWeb 端与 Node 端都通过cage.runCode(script, modules)把脚本送进 QuickJS 运行时执行见 src/web/test-runner/index.ts宿主只通过模块注入把受控 API 递给沙箱defaultModulespreRequestModule/postRequestModule定义在 src/cage-modules/支持 ESM 语法脚本可以顶层import、顶层await由 faraday-cage 的esmModuleLoader加载环境值进入沙箱前会做cloneDeep深拷贝脚本对入参的“意外修改”不会污染宿主对象。2. legacyWorkerWeb/ isolated-vmNodeWebsrc/web/pre-request/worker.ts 在独立 Web Worker 里用new Function(pw, preRequestScript)执行脚本——隔离级别较弱仍是同一 JS 引擎只能拿到pw命名空间脚本结束后 worker 立即terminate()Nodesrc/node/pre-request/legacy.ts 使用isolated-vm创建 V8 Isolate 与 Context把序列化后的 API 方法getSerializedAPIMethods注入 jail再用 Proxy 包装成可跨区调用的pw对象最后isolate.dispose()回收。这也是isolated-vm作为可选peerDependency 存在的原因——src/node/pre-request/index.ts 中 legacy 分支特意用动态import加载避免不需要 legacy 时引入这个需要原生编译的依赖。四、公开 API 详解Web 与 Node 两端导出同名函数runPreRequestScript与runTestScript见 web.d.ts / node.d.ts。它们都遵循脚本字符串 选项对象 → Eitherstring, 结果的签名。选项判别联合类型src/types/index.ts 用 TypeScript 判别联合把两种后端的能力差异编译期显式化export type RunPreRequestScriptOptions | { envs: TestResult[envs] request: HoppRESTRequest cookies: Cookie[] | null // Desktop App 独占 experimentalScriptingSandbox: true // 必须显式为 true hoppFetchHook?: HoppFetchHook // 自定义 fetch 实现 } | { envs: TestResult[envs] experimentalScriptingSandbox?: false }要点走新后端true时Pre-Request 必须额外提供request与cookies因为脚本可以改写请求对象和 CookieTest 脚本则额外提供request、response、cookieshoppFetchHook是宿主注入的 fetch 实现钩子。源码注释说明其动机Web 应用经 KernelInterceptorService 路由尊重拦截器设置CLI 则直接走网络请求沙箱内的hopp.fetch()/ 全局fetch最终都调用宿主提供的实现而不是浏览器原生 fetch见 src/bootstrap-code/pre-request.jslegacy 分支只需要envsWeb或envsresponseNode因为老后端不具备改写请求的能力。结果结构export type SandboxPreRequestResult { updatedEnvs: TestResult[envs] consoleEntries?: ConsoleEntry[] updatedRequest?: HoppRESTRequest updatedCookies: Cookie[] | null } export type SandboxTestResult { tests: TestDescriptor envs: TestResult[envs] consoleEntries?: ConsoleEntry[] updatedCookies: Cookie[] | null }src/types/index.ts其中测试结果是树形结构因为pm.test(..., fn)可以嵌套export type TestDescriptor { descriptor: string // 测试块名称 expectResults: ExpectResult[] children: TestDescriptor[] // 子测试块 } export type ExpectResult { status: pass | fail | error message: string }脚本执行时src/web/test-runner/index.ts 会先初始化一个根节点[{ descriptor: root, expectResults: [], children: [] }]pm.test的调用逐层把子块压入testRunStack最终随结果返回。consoleEntries则收集了沙箱内console.log等调用带时间戳与级别供宿主控制台展示。TestResponse递给测试脚本的响应快照export type TestResponse { status: number statusText: string responseTime: number headers: { key: string; value: string }[] body: string | object // JSON 内容类型时为解析后的对象 }响应对象在进入沙箱前会先过一道 preventCyclicObjects 检查src/web/test-runner/index.ts循环引用无法序列化跨入沙箱直接以Response marshalling failed: ...的 Left 结果拒绝执行避免在沙箱边界上挂死。五、执行流程与错误恢复experimental 后端以 Web 端runTestScript为例完整调用链是runTestScript ├─ preventCyclicObjects(response) // 1. 响应可序列化检查 ├─ parseScriptForSyntax(script, target) // 2. acorn 宿主侧语法预检 └─ runPostRequestScriptWithFaradayCage ├─ acquireCage() // 3. 获取单例QuickJS 沙箱 ├─ executeTestOnCage(...) // 4. 注入模块并 runCode │ └─ cage.runCode(script, [defaultModules, postRequestModule]) ├─ 若 bootstrap 失败 → resetCage 重试一次 └─ 返回 Eitherstring, SandboxTestResult语法预检在启动沙箱之前parseScriptForSyntax 用 acorn 按目标后端的语法档位解析脚本experimental 用sourceType: module允许顶层import与awaitlegacy 用sourceType: script两者都拒绝。这样做的收益是语法错误能在沙箱启动之前就以宿主侧友好消息暴露错误文案统一为Script execution failed: SyntaxError: ...见 src/node/test-runner/index.ts。沙箱单例与“基础设施错误”判定acquireCage 用一个模块级cagePromise缓存 FaradayCage 实例——QuickJS WASM 初始化不便宜Web 会话内复用同一个沙箱能显著降低每次执行的成本测试环境下VITESTtrue则默认每次新建保证用例间隔离。错误恢复的精髓在 isInfraError 的注释中FaradayCage/QuickJS errors arrive in two shapes:User script errors —cage.runCode()返回的result.err是 QuickJSdump()出来的普通对象不是instanceof ErrorInfrastructure errors — 宿主侧模块装配抛出的真实Error如 WASM 初始化失败、marshal 失败。instanceof Error可以可靠地区分这两者。执行函数据此做出不同处置src/web/test-runner/index.ts用户脚本错误→ 包装成Script execution failed: Name: message的 Left 结果沙箱本身健康无需重置基础设施错误 / bootstrap 失败→ 调用resetCage()丢弃单例返回哨兵值retry由外层重建全新沙箱再重试一次若第二次仍失败则报告sandbox initialization error (persistent)而不进入死循环。异步边界错误的“宿主上报器”faraday-cage 的 keepAlive 循环会吞掉被拒绝的 Promise导致脚本里的顶层await抛错无法通过常规错误通道传回宿主。项目的解法是一套前后呼应的机制脚本合并阶段下一节详述用__hoppReporter把整个脚本体包进try/catch沙箱引导代码src/bootstrap-code/pre-request.js以Object.defineProperty(globalThis, __hoppReportScriptExecutionError, { writable: false, configurable: false })定义不可篡改的上报器防止用户脚本删除或覆写它来“掩盖”错误执行结束后宿主检查captureHook.scriptExecutionError把上报的{ name, message, stack }转成 Left 结果。六、注入沙箱的模块与 pw / hopp / pm 三大命名空间cage.runCode的第二个参数是模块数组。defaultModules 为每个执行环境装配基础能力urlPolyfill/blobPolyfillQuickJS 里没有URL/Blob用 polyfill 补上Console模块接管console.*一边镜像输出到宿主控制台一边通过handleConsoleEntry回调把{ type, args, timestamp }收集为consoleEntriescustomCryptoModulesrc/cage-modules/crypto.ts桥接宿主globalThis.crypto让脚本能用 Web Crypto有专门测试目录 src/tests/cage-modules/crypto/esmModuleLoader支持顶层importcustomFetchModule以宿主传入的hoppFetchHook作为沙箱内fetch的实现encoding/timersBase64 编解码与定时能力。在此之上preRequestModule/postRequestModule通过 src/bootstrap-code/ 中的引导脚本pre-request.js、post-request.js在沙箱全局挂载三大命名空间命名空间定位代表 APIhoppHoppscotch 原生 APIhopp.env.set/get/delete/reset、hopp.env.global.*/hopp.env.active.*分层访问、hopp.request.setUrl/setHeader/setBody/setAuth、hopp.cookies.*、hopp.fetch()pw早期 Postwoman 兼容 APIpw.env.get/set/unset/resolve/getResolvepmPostman 兼容层pm.environment.*、pm.globals.*、pm.variables.replaceIn({{token}})、Postman 风格的pm.request.url/headers/query对象含 PropertyList 的add/upsert/find/insert等方法、pm.test()、pm.expect()几处值得注意的安全与兼容细节均可在 src/bootstrap-code/pre-request.js 中验证请求属性只读保护hopp.request的url/method/params/headers/body/auth全部用Object.defineProperty定义 gettersetter 抛出TypeError: hopp.request.prop is read-only最后再Object.freeze整个对象——脚本只能通过setUrl等显式 setter 改请求Postman 语义对齐pm.environment.get对缺失 key 返回undefined而非nullPostman 的行为pm.variables.replaceIn对{{key}}模板做正则替换且未命中时保留原文undefined/null 保真跨沙箱边界序列化时undefined与null会互相混淆项目在沙箱内外各维护一组哨兵字符串__HOPPSCOTCH_UNDEFINED__/__HOPPSCOTCH_NULL__定义于 src/constants/sandbox-markers.ts并在 bootstrap 代码中以“必须与之一致”的注释镜像pm.env.set(key, null)之类的调用因此能精确保留原始类型。此外 src/types/index.ts 定义了SandboxValue类型别名文档化了“值跨 QuickJS 边界会丢失 TypeScript 类型信息”这一事实并明确环境变量支持原始类型与可递归序列化的对象/数组不支持函数与 Symbol。脚本执行期间环境变量允许保存复杂类型SandboxEnvs但离开沙箱时经getUpdatedEnvs()统一序列化为字符串EnvironmentVariable消费方拿到的currentValue/initialValue恒为string。七、脚本级联合并从 Collection 树到单个可执行脚本Hoppscotch 允许在集合根目录、各级文件夹、单个请求上各写脚本执行时需要按“根 → 文件夹 → 请求”的级联顺序串联。combineScriptsWithIIFE 负责把多个脚本字符串合并为一个experimental每个脚本包成await (async function() {...})();顺序执行顶层import/export ... from声明被提升到模块作用域IIFE 内部不允许它们跨脚本相同的 import 去重同名但不同来源的 import 则生成友好的SyntaxError提示改名__hoppReporter、globalThis是保留名禁止作为 import 绑定legacy每个脚本包成;(function() {...}).call(this);同步执行顶层await在解析期即被拒绝行首的;是防 ASI 陷阱的防御。配套的 stripModulePrefix 处理 Monaco 编辑器的特性TS 模式缓冲区会前缀export {};进入 legacy非模块执行或导出集合 JSON 前必须剥掉stripJsonSerializedModulePrefix则处理 JSON 字符串里的转义形态。八、测试套件与开发工作流README 给出的开发流程以当前仓库为准# 1. 克隆仓库 git clone https://gitcode.com/GitHub_Trending/ho/hoppscotch # 2. 安装依赖pnpm workspace pnpm install # 3. 进入包目录 cd packages/hoppscotch-js-sandbox # 4. 构建vite build tsc --emitDeclarationOnly pnpm run build # 5. 运行测试vitest run pnpm run test测试命令实际映射到 package.json 中的test: vitest run测试环境为 Node见 vite.config.ts 的test.environment: node并加载 setupFiles.ts。src/tests/ 下有 60 余个按能力域组织的 spec 文件其目录结构本身就是一份“沙箱能力清单”pm-namespace/Postman 兼容层的逐方法覆盖——request/headers/propertylist.spec.ts、query/propertylist.spec.ts、url/helper-methods.spec.ts、response/datauri-comprehensive.spec.ts、variables.spec.ts、sendRequest.spec.ts、expect-fail.spec.ts等pw-namespace/expect/toBe.spec.ts、toBeType.spec.ts、base64-helper-functions.spec.tshopp-namespace/chai-powered-assertions/子目录core / strings-regex / properties-collections / functions-errors / exotic-objects、fetch.spec.ts、cookies.spec.ts、request.spec.ts、response.spec.tscage-modules/宿主注入模块测试包括fetch.spec.ts与crypto/下的加解密、签名验签、随机值、digest 五个文件combined/跨脚本行为——test-runner.spec.ts、script-imports.spec.ts、env-fallback-behavior.spec.ts、null-undefined-value-preservation.spec.ts、script-error-recovery.spec.ts对应上文的基础设施错误重试路径等utils/cage.spec.ts、shared.spec.ts等。这些用例与前述源码一一对应例如null-undefined-value-preservation.spec.ts验证哨兵标记机制script-error-recovery.spec.ts验证resetCage retry路径crypto/目录验证customCryptoModule桥接。九、版本与许可README 声明项目遵循 Semantic Versioning但由于仍处 pre-1.0代码与公开 API 均不承诺稳定许可为 MIT根目录 LICENSE 为准。十、小结可复用的沙箱设计要点从packages/hoppscotch-js-sandbox的实现可以提炼出几条做“执行用户代码”类功能时值得借鉴的实践默认强隔离、兼容弱隔离新后端QuickJS/WASM为默认路径legacy 后端保留并通过可选 peerDependency 动态 import 避免不必要的原生依赖负担编译期区分能力面用判别联合类型让“新后端才能改 request/cookies”这一事实写在类型里错误通道分离用instanceof Error区分用户脚本错误与宿主基础设施错误后者触发“重置沙箱 单次重试”的自愈策略能力最小注入沙箱内一切 API含fetch都由宿主以模块/钩子形式注入并可审计hopp.request属性只读 显式 setter跨边界语义保真哨兵标记保null/undefined、深拷贝隔离入参、循环引用预检、宿主侧 acorn 预解析把边界问题消灭在执行之前。如需进一步深入建议从 src/web/test-runner/index.ts完整执行链与 src/bootstrap-code/post-request.jspm.test/expect的挂载方式两个文件读起。【免费下载链接】hoppscotchOpen-Source API Development Ecosystem • https://hoppscotch.io • Offline, On-Prem Cloud • Web, Desktop CLI • Open-Source Alternative to Postman, Insomnia项目地址: https://gitcode.com/GitHub_Trending/ho/hoppscotch创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考