Skip to content

fix(poc-sandbox): namespace segmentation#8

Merged
jackkav merged 2 commits into
jackkav:claude/admiring-mcclintock-8be6befrom
Kong:sandbox-hardening
Jun 18, 2026
Merged

fix(poc-sandbox): namespace segmentation#8
jackkav merged 2 commits into
jackkav:claude/admiring-mcclintock-8be6befrom
Kong:sandbox-hardening

Conversation

@kwburns-kong

Copy link
Copy Markdown

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

  • Namespace segmentation: the host-injected __hostBridge / __crypto* / __hostConsole
    globals are captured into closure locals and deleted from globalThis at the end of the
    bootstrap, before plugin source evaluates. The closures keep the references they need, so plugin
    code can only reach the rebuilt context, never the raw bridge.
  • Host-side plugin identity: scopePluginDataHandlers forces the trusted pluginName onto
    every 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.
  • DoS guards: install a QuickJS interrupt handler (shouldInterruptAfterDeadline) plus a memory
    limit, so a synchronous while(true){} is interrupted within the deadline instead of hanging the
    process. The async driver deadline alone can't catch synchronous loops.
  • Bridge boundary hardening: util.render depth cap (capUtilRenderDepth); host error-message
    path redaction (sanitizeErrorMessage); prototype-pollution stripping
    (__proto__/constructor/prototype) on the parsed bridge body; request and response payload
    size caps (maxPayloadBytes, default 8 MiB).

Scope

Files

  • src/templating/sandbox/in-sandbox-bootstrap.ts — capture + delete raw host globals
  • src/templating/sandbox/host-bridge.tsscopePluginDataHandlers, capUtilRenderDepth
  • src/templating/sandbox/plugin-tag-sandbox.ts — interrupt + memory guards, prototype-pollution
    reviver, payload caps (maxPayloadBytes option)
  • src/templating/sandbox/marshal.tssanitizeErrorMessage in encodeBridgeFailure
  • src/main/templating-worker-database.ts — compose capUtilRenderDepth(scopePluginDataHandlers(...))
  • src/templating/sandbox/{namespace-segmentation,bridge-hardening}.test.ts — new tests

Tests / verification

  • namespace-segmentation.test.ts (6): __hostBridge hidden; crypto globals deleted but
    require('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 polluting Object.prototype; oversized request/response rejected.
  • Full sandbox suite 37 pass (21 parity unchanged + 6 + 10); ESLint + tsc clean on touched files.

Known limitations / follow-ups

  • Coverage boundary: only the worker→IPC execute handlers are gated; the in-process render()
    engine (createLiquidTag) and nested util.render still run unsandboxed. The depth cap bounds
    direct self-recursion but depth resets across the ungated engine.
  • WASM/dependency supply chain not yet pinned.
  • require allowlist still breaks plugins using fs/lodash/axios (inherited from PoC: run plugin template tags in a QuickJS-WASM sandbox Kong/insomnia#10095).

kwburns-kong and others added 2 commits June 17, 2026 13:14
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>
@jackkav jackkav merged commit 9b766a4 into jackkav:claude/admiring-mcclintock-8be6be Jun 18, 2026
@jackkav jackkav deleted the sandbox-hardening branch June 18, 2026 09:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants