PoC: run plugin template tags in a QuickJS-WASM sandbox#10095
Draft
jackkav wants to merge 5 commits into
Draft
Conversation
Behind a new `templateTagSandboxEnabled` setting (default off), route plugin template-tag execution through a QuickJS-WASM sandbox instead of invoking the plugin's `run()` directly in the main process. Approach (per PR Kong#10072): bulk-copy render state into the sandbox as JSON, rebuild the plugin `context` API in pure JS inside the sandbox, and bridge only async work back to the host via the existing `pluginToMainAPI` handlers. `node:crypto` is exposed as synchronous host functions so `require('crypto')` works without a sync/async mismatch. - templating/sandbox/: quickjs-runtime, marshal, host-bridge, in-sandbox-bootstrap, plugin-tag-sandbox (+ parity tests vs in-process tags and node:crypto) - main/templating-worker-database.ts: route execute handlers through the sandbox when the flag is on; legacy path unchanged otherwise - esbuild: keep quickjs-emscripten external so its .wasm resolves at runtime - settings + scripting-settings UI toggle - examples/insomnia-plugin-sandbox-demo: manual E2E fixture Scope: template tags only; sandbox runs in main. require shim covers path + crypto (other modules throw a clear error — follow-up work).
Three security fixes to the plugin template-tag sandbox (gated behind the default-off templateTagSandboxEnabled setting): - Namespace segmentation: capture the host-injected __hostBridge / __crypto* / __hostConsole globals into closure locals and delete them from globalThis at the end of the bootstrap, so plugin code can reach only the rebuilt context object, not the raw bridge. - Host-side plugin identity: add scopePluginDataHandlers() to force the trusted pluginName onto pluginData.* bridge calls, preventing a tag from forging another plugin's name to read/clear its store. - DoS guards: install an interrupt handler (shouldInterruptAfterDeadline) and a memory limit on the QuickJS runtime so a synchronous infinite loop is interrupted within the deadline instead of hanging the process. Adds namespace-segmentation.test.ts covering the above (global hidden, direct-bridge denial, cross-plugin forge override, infinite-loop interrupt). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Four defensive fixes at the main-side sandbox bridge (all within the PR's "only main-side execute handlers change" scope): - util.render depth cap (capUtilRenderDepth): reject nested util.render once the sandbox-reported depth exceeds a limit; a cheap guard layered on the runtime interrupt/memory limits. - Error-message sanitization: redact absolute host filesystem paths from bridge error messages before they cross back into the sandbox. - Prototype-pollution guard: strip __proto__/constructor/prototype keys when JSON-parsing the attacker-controlled bridge body. - Payload size caps: reject oversized request/response bridge payloads (configurable via maxPayloadBytes; defaults to 8 MiB). Adds bridge-boundary-validation.test.ts (10 cases) covering each fix. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fix(poc-sandbox): namespace segmentation
Toggle the templateTagSandboxEnabled setting and verify a user plugin
template tag executes inside the QuickJS-WASM sandbox with a working
node crypto shim. Asserts the tag flips from main-process to sandbox
execution while producing an identical sha256 digest via require('crypto').
Adds a data-testid to the Scripting settings toggle for a stable locator.
Contributor
Author
E2E verified against a fully-packaged build ✅Added
This proves the toggle does what it claims and the Results
The packaged run is the important one here — it exercises the externalized QuickJS
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why
Plugin template tags (
getTemplateTags) currently execute with full, unsandboxed Node access in the main process — the worker routes each tag'srun()over IPC tomain/templating-worker-database.ts, which calls it directly. This PoC runs thatrun()inside a QuickJS-WASM sandbox instead, exposing a single bridged plugin API. Templating itself never evals code (LiquidJS is pure string work) — only pluginrun()functions need isolation.Based on the marshaling approach validated in #10072.
Approach
contextAPI is rebuilt in pure JS inside the sandbox; only genuinely-async work bridges back to the host via the existingpluginToMainAPIhandlers — reused verbatim.executePendingJobs), not the asyncified variant (spike: QuickJS-WASM sandbox marshaling cost harness #10072 found asyncify breaks on suspended awaits).node:cryptovia sync host functions. Plugins call crypto synchronously (createHash(x).digest()), so it can't go through the async bridge. Since the sandbox runs in main (Node), crypto is exposed as synchronous host functions backed by realnode:crypto— correct + secure, no hand-rolled crypto. (Revisit when/if the sandbox moves off the main process.).wasmresolves at runtime (bundling broke__dirnameresolution).Scope
templateTagSandboxEnabledsetting (default off), toggle in Scripting settings. Legacy path untouched when off.requireshim coverspathandcrypto; anything else throws a clearCannot find module 'X' in sandbox(npm deps / relative requires / other builtins are follow-up work).Files
src/templating/sandbox/—quickjs-runtime,marshal,host-bridge,in-sandbox-bootstrap,plugin-tag-sandbox(+ tests)src/main/templating-worker-database.ts— route execute handlers through the sandbox behind the flagesbuild.entrypoints.ts— quickjs externalized in the main buildinsomnia-data) +scripting-settings.tsxtoggleexamples/insomnia-plugin-sandbox-demo/— manual E2E fixtureTests / verification
src/templating/sandbox/plugin-tag-sandbox.test.ts— 21 cases: base64 parity (exercises btoa/atob/TextEncoder/TextDecoder polyfills),osasync-bridge round-trip,require('crypto')parity vsnode:crypto(sha256/md5/sha1/sha512/hmac, randomUUID/randomBytes), and error propagation. All pass.examples/insomnia-plugin-sandbox-demo, toggle the flag, render{% sandboxprobe 'hi' %}— output flips betweenran in: main-processandran in: sandbox.Known limitations / follow-ups
requireshim is minimal (path + crypto) — broaden coverage (npm deps, relative files, more builtins).Response/Buffer marshaling is stubbed;util.renderrecursion bridges but is lightly exercised.