Skip to content

feat(templating): (T1) flip trust model — pluginSandboxEnabled + per-plugin elevated opt-in - #10318

Open
jackkav wants to merge 5 commits into
developfrom
claude/sandbox-pr12-trust-flip
Open

feat(templating): (T1) flip trust model — pluginSandboxEnabled + per-plugin elevated opt-in#10318
jackkav wants to merge 5 commits into
developfrom
claude/sandbox-pr12-trust-flip

Conversation

@jackkav

@jackkav jackkav commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

PR 12 — T1: flip the trust model (pluginSandboxEnabled + per-plugin elevated opt-in)

Phase 2 capstone. With every untrusted surface now routable through the sandbox (L1/H1/A1), this makes the sandbox the default for user plugins and adds a deliberate, per-plugin escape hatch.

What it ships

  • New setting pluginSandboxEnabled (default off) that supersedes/absorbs templateTagSandboxEnabled: either flag on activates the sandbox, so existing template-tag opt-ins keep working (migration bridge).
  • Per-plugin pluginConfig.elevated escape hatch: a user plugin marked elevated runs in-process with full host access even while the sandbox is on.
  • One central resolver (common/plugins/sandbox-mode.ts: isSandboxEnabled / resolvePluginExecutionMode / shouldSandboxPlugin) replacing the templateTagSandboxEnabled && directory !== '' condition that was duplicated across 7 gate sites (load discovery, request/response hooks × plugin-window + node runtime, actions, user template tags). Dependency-free so main, the plugin window, and the inso CLI share it. Unit-tested (12 cases).
  • Preferences UI: a "Sandbox all plugin code" toggle in Scripting; per-plugin execution-mode indicator (Sandboxed / Elevated / In-process) and a "Full host access" checkbox in Plugins.

Trust model

  • Bundle/first-party plugins (directory === '') are always trusted → in-process (unchanged; unsafePluginMainActions by design).
  • User plugins: default-deny → sandboxed when the sandbox is on; elevated opts an individual one back to in-process. Load-time discovery is now per-plugin (an elevated plugin is nodeRequired so its hooks/actions/tags are live functions).

Tests

  • Unit: sandbox-mode.test.ts — flag supersession, mode resolution, the gate boolean.
  • E2E: pluginSandboxEnabled sandboxes a user plugin's action (canary ranin-sandboxed); toggling Full host access flips the mode indicator to Elevated and the same action then runs in-process (ranin-mainprocess).

Validation

tsc clean (insomnia + insomnia-data), 170 sandbox+resolver unit tests green, eslint clean. Default-off → zero behavior change until a flag is toggled.

Builds on the merged L1/H1/A1 work now on develop. Follow-up: PR 13 (S1) capability/bridge-handler scoping audit — see the plan gist.

🤖 Authored by Claude Code

@github-actions

github-actions Bot commented Jul 31, 2026

Copy link
Copy Markdown

✅ Circular References Report

Generated at: 2026-08-05T10:15:56.379Z
Status: ✅ NO CHANGE

Summary

Metric Base (develop) PR Change
Total Circular References 9 9 0 (0.00%)
Click to view all circular references in PR (9)
insomnia-inso/src/db/models/types.ts -> insomnia-inso/src/db/types.ts
insomnia/src/main/prompt-bridge.ts -> insomnia/src/main/window-utils.ts -> insomnia/src/main/plugin-window.ts
insomnia/src/main/window-utils.ts -> insomnia/src/main/plugin-window.ts
insomnia/src/network/network.ts -> insomnia-scripting-environment/src/objects/index.ts -> insomnia-scripting-environment/src/objects/collection.ts -> insomnia-scripting-environment/src/objects/response.ts
insomnia/src/network/network.ts -> insomnia/src/common/render.ts
insomnia/src/ui/components/settings/import-export.tsx -> insomnia/src/ui/components/modals/export-requests-modal.tsx
insomnia/src/ui/components/tabs/tab-list.tsx -> insomnia/src/ui/components/tabs/tab.tsx
insomnia/src/ui/components/templating/tag-editor-arg-sub-form.tsx -> insomnia/src/ui/components/templating/external-vault/external-vault-form.tsx
insomnia/src/ui/components/viewers/response-viewer.tsx -> insomnia/src/ui/components/viewers/response-multipart-viewer.tsx
Click to view all circular references in base branch (9)
insomnia-inso/src/db/models/types.ts -> insomnia-inso/src/db/types.ts
insomnia/src/main/prompt-bridge.ts -> insomnia/src/main/window-utils.ts -> insomnia/src/main/plugin-window.ts
insomnia/src/main/window-utils.ts -> insomnia/src/main/plugin-window.ts
insomnia/src/network/network.ts -> insomnia-scripting-environment/src/objects/index.ts -> insomnia-scripting-environment/src/objects/collection.ts -> insomnia-scripting-environment/src/objects/response.ts
insomnia/src/network/network.ts -> insomnia/src/common/render.ts
insomnia/src/ui/components/settings/import-export.tsx -> insomnia/src/ui/components/modals/export-requests-modal.tsx
insomnia/src/ui/components/tabs/tab-list.tsx -> insomnia/src/ui/components/tabs/tab.tsx
insomnia/src/ui/components/templating/tag-editor-arg-sub-form.tsx -> insomnia/src/ui/components/templating/external-vault/external-vault-form.tsx
insomnia/src/ui/components/viewers/response-viewer.tsx -> insomnia/src/ui/components/viewers/response-multipart-viewer.tsx

Analysis

No Change: This PR does not introduce or remove any circular references.


This report was generated automatically by comparing against the develop branch.

@jackkav
jackkav marked this pull request as ready for review August 3, 2026 12:36
Copilot AI review requested due to automatic review settings August 3, 2026 12:36
jackkav added 3 commits August 3, 2026 14:36
…-in, centralized gate

Introduces the trust-model flip's core logic and wires every untrusted-execution surface to it.

- New setting `pluginSandboxEnabled` (default off) that supersedes/absorbs `templateTagSandboxEnabled`:
  either flag on activates the sandbox, so existing template-tag opt-ins keep working (migration bridge).
- New per-plugin `pluginConfig.elevated` escape hatch: a user plugin marked elevated runs in-process
  with full host access even while the sandbox is on. Widened PluginConfig/PluginConfigMap/Plugin.config.
- New pure resolver `common/plugins/sandbox-mode.ts` (isSandboxEnabled / resolvePluginExecutionMode /
  shouldSandboxPlugin) — the single source of truth replacing the 7 duplicated
  `templateTagSandboxEnabled && directory !== ''` conditions. Dependency-free so main, the plugin
  window, and the inso CLI node runtime all share it. Unit-tested (12 cases).
- Wired all surfaces to shouldSandboxPlugin: load-time discovery (now per-plugin, so an elevated
  plugin is nodeRequire-d for live functions), request/response hooks (plugin-window + node runtime),
  actions, and user template tags. Bundle-tag path reads isSandboxEnabled (bundle stays trusted).

No UI yet (next commit); default-off means no behavior change until a flag is toggled.
…lugin elevated + mode indicator

- Scripting settings: new "Sandbox all plugin code (experimental)" toggle for pluginSandboxEnabled
  (data-testid toggle-plugin-sandbox), beside the existing template-tag toggle.
- Plugins settings: each user plugin card now shows its resolved execution mode
  (Sandboxed / Elevated / In-process, data-testid plugin-mode-<name>) and a "Full host access"
  checkbox (data-testid plugin-elevated-<name>) that writes pluginConfig.<name>.elevated. Mode +
  toggle read from live settings so they update immediately, before the plugin list reloads.
- Widened SerializablePlugin.config to carry the optional `elevated` flag through the bridge.
…ugin; elevating runs it in-process

Composes the trust-flip's two user-visible behaviors on one action-probe plugin:
- Enabling the new pluginSandboxEnabled toggle (not the legacy template-tag flag) routes the user
  plugin's action into the sandbox (canary reports ranin-sandboxed).
- Toggling "Full host access" in Preferences → Plugins flips the mode indicator to "Elevated" and the
  same action then runs in-process (marker absent, ranin-mainprocess) — the per-plugin escape hatch.
Reuses the sandbox-action-collection.yaml fixture and the established writePlugin/clearPluginToast
helpers; adds enablePluginSandbox + setPluginElevated helpers.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Flips the plugin trust model toward sandbox-by-default by introducing a new pluginSandboxEnabled setting plus a per-plugin elevated escape hatch, and centralizes sandbox gating logic so all plugin execution surfaces agree.

Changes:

  • Adds pluginSandboxEnabled setting (default false) and UI toggle in Scripting settings.
  • Introduces a shared resolver (common/plugins/sandbox-mode.ts) used across plugin discovery, actions, hooks, and template tags, plus unit tests.
  • Adds per-plugin “Full host access” (pluginConfig.elevated) toggle + execution-mode indicator in Preferences → Plugins, with new smoke coverage.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
packages/insomnia/src/ui/components/settings/scripting-settings.tsx Adds Preferences → Scripting toggle for pluginSandboxEnabled.
packages/insomnia/src/ui/components/settings/plugins.tsx Adds per-plugin elevated toggle + mode badge using the shared resolver.
packages/insomnia/src/runtimes/network/network-adapter.node.ts Routes request/response hooks through shouldSandboxPlugin (now per-plugin).
packages/insomnia/src/plugins/invoke-method.ts Uses shouldSandboxPlugin for actions and hooks routing via templating-worker DB.
packages/insomnia/src/plugins/index.ts Makes load-time discovery per-plugin (sandbox vs nodeRequire) using shared resolver.
packages/insomnia/src/main/templating-worker-database.ts Uses new resolver/flag supersession for template-tag execution routing.
packages/insomnia/src/common/plugins/types.ts Extends plugin config type with optional elevated.
packages/insomnia/src/common/plugins/sandbox-mode.ts New single source of truth for sandbox enablement + per-plugin execution mode.
packages/insomnia/src/common/plugins/sandbox-mode.test.ts Adds unit tests for flag supersession + mode resolution + gating boolean.
packages/insomnia/src/common/plugins/bridge-types.ts Extends serialized plugin config to include optional elevated.
packages/insomnia-smoke-test/tests/smoke/sandbox-template-tags.test.ts Adds E2E covering pluginSandboxEnabled + per-plugin elevation flip.
packages/insomnia-data/src/models/settings.ts Adds pluginSandboxEnabled default to settings init.
packages/insomnia-data/common-src/settings.ts Adds pluginSandboxEnabled and extends PluginConfigMap with elevated.
Suppressed comments (1)

packages/insomnia/src/runtimes/network/network-adapter.node.ts:139

  • Same as applyRequestHooks: services.settings.get() is awaited even when canSandbox is false (inso CLI). This adds avoidable I/O/CPU for a path that will always run hooks in-process.
  // where the sandbox is unavailable and hooks run in-process — gate on process.type accordingly.
  const canSandbox = !!process.type;
  const settings = await services.settings.get();
  const hookIndexByPlugin: Record<string, number> = {};

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/insomnia/src/runtimes/network/network-adapter.node.ts
Comment thread packages/insomnia/src/main/templating-worker-database.ts
@jackkav

jackkav commented Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

Plugin sandbox trust model (T1) — reference

Documenting the trust model this PR establishes, for future maintainers. This is the state after T1; the source of truth in code is common/plugins/sandbox-mode.ts.

The two questions, kept separate

  1. Is the sandbox on at all?isSandboxEnabled(settings)
  2. Should this plugin's code run in it?resolvePluginExecutionMode(settings, plugin) / shouldSandboxPlugin(...)

Before T1 the answer to (2) was the condition templateTagSandboxEnabled && plugin.directory !== '', duplicated across 7 call sites. T1 centralizes it into one resolver so every surface agrees.

Flags (question 1)

  • templateTagSandboxEnabled — the Phase-1 experiment flag (template tags only, historically).
  • pluginSandboxEnabled — T1's flag: all untrusted surfaces.
  • Supersession: isSandboxEnabled = pluginSandboxEnabled || templateTagSandboxEnabled. Either flag on activates the sandbox, so anyone who opted into the template-tag experiment keeps sandboxing without re-toggling. Both default false.

Execution modes (question 2)

resolvePluginExecutionMode(settings, plugin) returns exactly one of:

Mode When Runs where Host access
trusted plugin.directory === '' (bundle / first-party) in-process full (by design — e.g. external-vault's unsafePluginMainActions)
sandboxed user plugin, sandbox on, not elevated QuickJS sandbox only declared capabilities, via the bridge
elevated user plugin, sandbox on, pluginConfig.<name>.elevated === true in-process full (deliberate per-plugin opt-in)
unsandboxed user plugin, sandbox off in-process full (legacy behavior — not a trust decision)

shouldSandboxPlugin(...) is just mode === 'sandboxed' — the single boolean every surface reads.

The seams

  • Bundle vs user: directory === ''. Bundle plugins are shipped by us and always trusted. Only user plugins are ever sandboxed.
  • The escape hatch: pluginConfig.<name>.elevated — a per-plugin toggle in Preferences → Plugins ("Full host access"), off by default, surfaced with a mode indicator. This is the deliberate, visible trust decision for the rare community plugin that genuinely needs native/host access. It is per-plugin and never global.
  • Default-deny: a user plugin with the sandbox on and no elevation is sandboxed, and inside the sandbox both axes are default-deny — modules (the require allowlist) and capabilities (the host bridge). Declaring neither ⇒ baseline only.

The 7 gated surfaces (all now read shouldSandboxPlugin)

  1. Load-time discovery (plugins/index.ts) — a sandboxed plugin's exports are discovered by evaluating its source inside the sandbox (no host execution at install/enable). An elevated/unsandboxed plugin is nodeRequired so its hooks/actions/tags are live in-process functions. The decision is per-plugin because elevated is per-plugin.
  2. Template tags — user tags (plugin.executeUserPluginTag); bundle tags run in the sandbox with the broad all-modules/all-caps profile when the flag is on (isSandboxEnabled), which is a hardening, not a trust downgrade.
    3–4. Request / response hooks — plugin-window path (invoke-method.ts) and node-runtime path (network-adapter.node.ts).
  3. Actions — request/group/workspace/document (invoke-method.ts).

Security invariants (unchanged by T1, relied on by it)

Caveats worth remembering

  • elevated = full trust. An elevated plugin runs in-process with full Node — identical to the pre-sandbox world. It's the user's explicit choice; there is no partial elevation.
  • The inso CLI has no sandbox. The node runtime gates sandbox routing on !!process.type; in the pure-Node CLI (no Electron) the sandbox host is unavailable, so user-plugin hooks run in-process there regardless of the flag. CLI users are trusting their own plugins.
  • Bundle template tags run sandboxed-with-all-caps when the flag is on. That's for consistency/hardening; it does not change that bundle plugins are trusted (their hooks/actions/load still run in-process).
  • Flag honesty: only pluginSandboxEnabled means "every untrusted surface is isolated." templateTagSandboxEnabled historically scoped to tags; the supersession keeps it working but new UI copy should refer to the plugin-wide flag.

@kwburns-kong kwburns-kong left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two fixes, PR #10326.

)

* fix(templating): close two plugin-registry trust-cache gaps

- pluginConfig.elevated is keyed by declared plugin name, not by folder;
  a same-named folder placed alongside an already-elevated plugin
  inherited its trust grant and ran in-process before any collision was
  even noticed. traversePluginPath now pre-scans for duplicate names
  (order-independent) and refuses to load any colliding folder.

- applyRequestHooks/applyResponseHooks tagged a caught error with
  `error.plugin = plugin`, a plain assignment that a plugin-thrown Error
  could intercept via its own `plugin` property setter, handing the hook
  a live, mutable reference to its own cached registry entry and letting
  it flip `directory`/`config.elevated` to defeat later sandboxing.
  Switched to Object.defineProperty, which bypasses any such setter.

* fix(plugins): use relative-path containment check instead of startsWith

A bare .startsWith(base) on a resolved path accepts a sibling directory
whose name happens to prefix-match the base (e.g. /plugins-evil vs
/plugins). Added a shared isContainedIn helper (path.relative, rejects
.. or an absolute result) and applied it to both the existing
plugin-path containment check and the new duplicate-name pre-pass.
}
try {
// package.json is plain data — reading it runs no plugin code.
const pluginJson = getNodeRequire()(path.resolve(modulePath, 'package.json'));

@aikido-pr-checks aikido-pr-checks Bot Aug 5, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential file inclusion attack via reading file - high severity
If an attacker can control the input leading into the ReadFile function, they might be able to read sensitive files and launch further attacks with that information.

Show fix
Suggested change
const pluginJson = getNodeRequire()(path.resolve(modulePath, 'package.json'));
const packageJsonPath = path.resolve(modulePath, 'package.json');
const relativeToModule = path.relative(modulePath, packageJsonPath);
if (relativeToModule.startsWith('..') || path.isAbsolute(relativeToModule)) {
continue;
}
const pluginJson = getNodeRequire()(packageJsonPath);

Reply @AikidoSec ignore: [REASON] to ignore this issue.
More info

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@AikidoSec ignore: False positive. The read target is a constant path.resolve(modulePath, 'package.json') where modulePath is a readdir-sourced entry already validated by isContainedIn(p, modulePath) two lines above; the trailing segment is the literal package.json, so it cannot escape and is not attacker-controlled. The suggested path.relative re-check can never fail for a constant child segment. Mirrors the pre-existing package.json read in the sibling traversePluginPath.

🤖 Addressed by Claude Code

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Based on your feedback, we ignored this issue because of the following reason:

False positive. The read target is a constant path.resolve(modulePath, 'package.json') where modulePath is a readdir-sourced entry already validated by isContainedIn(p, modulePath) two lines above; the trailing segment is the literal package.json, so it cannot escape and is not attacker-controlled. The suggested path.relative re-check can never fail for a constant child segment. Mirrors the pre-existing package.json read in the sibling traversePluginPath.

🤖 Addressed by Claude Code

@jackkav
jackkav enabled auto-merge (squash) August 5, 2026 10:03
…, scope bundle-tag sandbox to legacy flag

- network-adapter.node.ts: only read services.settings.get() when canSandbox (Electron). The pure-Node
  inso CLI can never reach the sandbox host, so the read was wasted work; pass undefined otherwise
  (shouldSandboxPlugin treats it as off). Both applyRequestHooks and applyResponseHooks.
- templating-worker-database.ts: bundle (first-party/trusted) template tags now sandbox only under the
  legacy templateTagSandboxEnabled experiment, not the new pluginSandboxEnabled. The T1 flag isolates
  *untrusted* plugins; sandboxing trusted bundle tags under it was an unintended behavior change.
  Dropped the now-unused isSandboxEnabled import.
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.

3 participants