fix(poc-sandbox): namespace segmentation#8
Merged
jackkav merged 2 commits intoJun 18, 2026
Conversation
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>
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
Builds on Kong#10095, which runs plugin template tags in a QuickJS-WASM sandbox. That PoC
isolates code execution, but a security pass found two gaps: the host bridge still passed
untrusted data through unchecked, and the sandbox's raw host functions stayed reachable on the
global scope. There were also no CPU or memory limits, so a tag could hang the process. This PR
closes those gaps so the sandbox is an actual trust boundary.
Approach
__hostBridge/__crypto*/__hostConsoleglobals are captured into closure locals and deleted from
globalThisat the end of thebootstrap, before plugin source evaluates. The closures keep the references they need, so plugin
code can only reach the rebuilt
context, never the raw bridge.scopePluginDataHandlersforces the trustedpluginNameontoevery
pluginData.*call, so a tag can't forge another plugin's name to read or clear its store.Identity comes from the host, never from the sandbox body.
shouldInterruptAfterDeadline) plus a memorylimit, so a synchronous
while(true){}is interrupted within the deadline instead of hanging theprocess. The async driver deadline alone can't catch synchronous loops.
util.renderdepth cap (capUtilRenderDepth); host error-messagepath redaction (
sanitizeErrorMessage); prototype-pollution stripping(
__proto__/constructor/prototype) on the parsed bridge body; request and response payloadsize caps (
maxPayloadBytes, default 8 MiB).Scope
unchanged — only main-side execute handlers and the sandbox internals change.
templateTagSandboxEnabledstays default-off; legacy path untouched when off.requireshim stillpath+cryptoonly.Files
src/templating/sandbox/in-sandbox-bootstrap.ts— capture + delete raw host globalssrc/templating/sandbox/host-bridge.ts—scopePluginDataHandlers,capUtilRenderDepthsrc/templating/sandbox/plugin-tag-sandbox.ts— interrupt + memory guards, prototype-pollutionreviver, payload caps (
maxPayloadBytesoption)src/templating/sandbox/marshal.ts—sanitizeErrorMessageinencodeBridgeFailuresrc/main/templating-worker-database.ts— composecapUtilRenderDepth(scopePluginDataHandlers(...))src/templating/sandbox/{namespace-segmentation,bridge-hardening}.test.ts— new testsTests / verification
namespace-segmentation.test.ts(6):__hostBridgehidden; crypto globals deleted butrequire('crypto')still works; direct bridge call throws; cross-plugin forge overridden;synchronous infinite loop interrupted.
bridge-hardening.test.ts(10): depth-cap unit + end-to-end; path redaction (POSIX/Windows/URL);__proto__stripped without pollutingObject.prototype; oversized request/response rejected.tscclean on touched files.Known limitations / follow-ups
render()engine (
createLiquidTag) and nestedutil.renderstill run unsandboxed. The depth cap boundsdirect self-recursion but
depthresets across the ungated engine.requireallowlist still breaks plugins usingfs/lodash/axios(inherited from PoC: run plugin template tags in a QuickJS-WASM sandbox Kong/insomnia#10095).