Files
insomnia/packages/insomnia-data/common-src/settings.ts
Jack Kavanagh cc9288c218 feat(templating): QuickJS template-tag sandbox behind a flag, with e2e canary (#10207)
* feat(templating): PoC run plugin template tags in a QuickJS-WASM sandbox

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 #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).

* test(smoke): e2e canary for the template-tag sandbox flag

Installs an inline probe plugin, renders its tags via the tag editor Live
Preview, and asserts the execution path flips main-process -> sandbox when
templateTagSandboxEnabled is toggled in Preferences > Scripting, with a
require('crypto') sha256 workload staying byte-identical across both paths.

* test(sandbox): suppress hardcoded-hmac-key semgrep finding on parity fixture

The HMAC key is a test vector for sandbox-vs-node:crypto parity, not a
credential; rename it to make that self-evident and add the repo-standard
nosemgrep suppression.

* fix(templating): contain sandbox plugin entry resolution to the plugin directory

Reject a package.json "main" that resolves outside the plugin's own folder
and bundled-plugin names that look like paths, so the sandbox source loader
cannot be steered into reading arbitrary files.

* fix(review): inline nosemgrep placement, plugin-load error context, cross-arch-safe canary

- Move the hardcoded-hmac-key suppression onto the flagged line (line-above
  placement was not honored by the scanner).
- Wrap getPluginEntrySource failures with the plugin name for diagnosability.
- Derive the canary's expected arch from the Electron main process instead of
  the Playwright runner so cross-arch setups can't flake the assertion.

* sec(templating): QuickJS template-tag sandbox additions  (#10209)

* fix(sandbox): enforce timeout on synchronous plugin loops

QuickJS's executePendingJobs() blocks the host thread until a synchronous
call returns, so the wall-clock deadline in drivePromiseToString was never
checked during a tight sync loop in plugin code, hanging the Electron main
process indefinitely. Add a QuickJS interrupt handler, which is polled
during synchronous execution, to enforce the deadline.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(sandbox): clamp crypto.randomBytes size to prevent OOM

hostCrypto.randomBytes(size) passed the sandboxed number straight to
Node's crypto.randomBytes with no upper bound, letting a plugin request
a multi-GB allocation (e.g. crypto.randomBytes(2 ** 31)) and crash the
host process. Clamp to 64KB before the call.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(sandbox): cap QuickJS heap to prevent unbounded allocation

QuickJS.newContext() had no memory limit, so a plugin allocating without
bound could exhaust the WASM heap and crash the host process. Set a 32MB
ceiling via ctx.runtime.setMemoryLimit().

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(sandbox): resolve symlinks before validating plugin entry path

getPluginEntrySource's containment check compared raw path strings, so a
plugin directory with a symlinked entry (e.g. index.js -> ../../../etc/secret)
passed the check while fs.readFileSync followed the symlink and read the
out-of-directory target. Re-run the check against fs.realpathSync'd paths.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(sandbox): close util.render sandbox escape

context.util.render() bridged to the shared render() pipeline, whose Liquid
engine dispatches any registered tag's real run() directly, in-process,
regardless of templateTagSandboxEnabled. A sandboxed plugin could hand it a
string containing "{% anyTag %}" (including its own tag) and have that tag
execute completely unsandboxed. Verified with a PoC that reached
child_process execution from inside a plugin tag with no require() or Node
access.

util.render is now restricted to plain {{ variable }} interpolation (the
only real existing use, confirmed against all built-in tag call sites) via
a second Liquid engine with no tags registered; {% tag %} syntax now fails
to parse instead of dispatching. Default render() behavior is unchanged for
every other caller.

Also drops the dead renderDepth field's misleading doc comment: within one
sandboxed execution the envelope's renderDepth is always 0, so depth could
never exceed 1 regardless of enforcement — it couldn't have caught this
recursion anyway.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(sandbox): fixed linting issue

* fix(sandbox): fixed linting issue

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: kwburns-kong <kyle.burns@konghq.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-06 19:01:48 +00:00

180 lines
5.1 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { ValueOf } from 'type-fest';
/**
* The readable definition of a hotkey.
*/
export interface KeyboardShortcutDefinition {
id: string;
description: string;
}
/**
* The combination of key presses that will activate a hotkey if pressed.
*/
export interface KeyCombination {
ctrl?: boolean;
alt?: boolean;
shift?: boolean;
meta?: boolean;
keyCode: number;
}
/**
* The collection of a hotkey's key combinations for each platforms.
*/
export interface PlatformKeyCombinations {
macKeys: KeyCombination[];
// The key combinations for both Windows and Linux.
winLinuxKeys: KeyCombination[];
}
export type KeyboardShortcut =
| 'workspace_showSettings'
| 'request_showSettings'
| 'preferences_showKeyboardShortcuts'
| 'preferences_showGeneral'
| 'request_quickSwitch'
| 'plugin_reload'
| 'showAutocomplete'
| 'request_send'
| 'request_showOptions'
| 'environment_showEditor'
| 'environment_showSwitchMenu'
| 'request_toggleHttpMethodMenu'
| 'request_toggleHistory'
| 'request_focusUrl'
| 'request_showGenerateCodeEditor'
| 'sidebar_focusFilter'
| 'sidebar_showCreateDropdown'
| 'sidebar_toggle'
| 'response_focus'
| 'showCookiesEditor'
| 'request_createHTTP'
| 'request_showDelete'
| 'request_showCreateFolder'
| 'request_showDuplicate'
| 'request_togglePin'
| 'environment_showVariableSourceAndValue'
| 'beautifyRequestBody'
| 'graphql_explorer_focus_filter'
| 'close_tab'
| 'tab_nextTab'
| 'tab_previousTab'
| 'tab_reopenClosedTab'
| 'request_openInNewTab';
/**
* The collection of defined hotkeys.
* The registry maps a hotkey by its reference id to its key bindings.
*/
export type HotKeyRegistry = Record<KeyboardShortcut, PlatformKeyCombinations>;
// HTTP version codes
export const HttpVersions = {
V1_0: 'V1_0',
V1_1: 'V1_1',
V2PriorKnowledge: 'V2PriorKnowledge',
V2_0: 'V2_0',
v3: 'v3',
default: 'default',
} as const;
export type HttpVersion = ValueOf<typeof HttpVersions>;
export enum UpdateChannel {
stable = 'stable',
beta = 'beta',
}
/** Gets a subset of Settings where the values match a condition */
export type SettingsOfType<MatchType> = NonNullable<
{
[Key in keyof Settings]: Settings[Key] extends MatchType ? Key : never;
}[keyof Settings]
>;
export interface PluginConfig {
disabled: boolean;
}
export type PluginConfigMap = Record<string, { disabled: boolean }>;
export interface Settings {
autoDetectColorScheme: boolean;
autoHideMenuBar: boolean;
autocompleteDelay: number;
clearOAuth2SessionOnRestart: boolean;
darkTheme: string;
deviceId: string | null;
disableHtmlPreviewJs: boolean;
disableResponsePreviewLinks: boolean;
/** If true, Insomnia wont show a notification when new updates are available. Users can still check for updates in Preferences. */
disableUpdateNotification: boolean;
enableKeyMapForInlineTextEditors: boolean;
editorFontSize: number;
editorIndentSize: number;
editorIndentWithTabs: boolean;
editorKeyMap: string;
editorLineWrapping: boolean;
/** If true, Insomnia will send anonymous data about features and plugins used. */
enableAnalytics: boolean;
filterResponsesByEnv: boolean;
followRedirects: boolean;
fontInterface: string | null;
fontMonospace: string | null;
fontSize: number;
fontVariantLigatures: boolean;
forceVerticalLayout: boolean;
hasKonnectPat: boolean;
konnectOrganizationId: string | null;
hotKeyRegistry: HotKeyRegistry;
httpProxy: string;
httpsProxy: string;
showVariableSourceAndValue: boolean;
lightTheme: string;
lineWrapping?: boolean;
maxHistoryResponses: number;
maxRedirects: number;
maxTimelineDataSizeKB: number;
noProxy: string;
nunjucksPowerUserMode: boolean;
pluginConfig: PluginConfigMap;
pluginNodeExtraCerts: string;
pluginPath: string;
preferredHttpVersion: HttpVersion;
proxyEnabled: boolean;
showPasswords: boolean;
theme: string;
timeout: number;
updateAutomatically: boolean;
updateChannel: UpdateChannel;
useBulkHeaderEditor: boolean;
useBulkParametersEditor: boolean;
validateAuthSSL: boolean;
validateSSL: boolean;
// vault related settings
saveVaultKeyLocally: boolean;
enableVaultInScripts: boolean;
saveVaultKeyToOSSecretManager: boolean;
vaultSecretCacheDuration: number;
dataFolders: string[];
// AST and shadowing check.
scriptSandboxEnabled: boolean;
// Wraps the user script in 'use strict', preventing accidental globals and making `this` undefined.
scriptStrictModeEnabled: boolean;
// Experimental: execute plugin template tags inside the QuickJS-WASM sandbox instead of directly in the main process.
templateTagSandboxEnabled: boolean;
// Names of security rules that have been individually disabled.
disabledSecurityRules: string[];
// AST blocked-property names that have been individually disabled.
disabledBlockedProperties: string[];
// AST blocked-root names that have been individually disabled.
disabledBlockedRoots: string[];
/** Custom npm registry URL for plugin installation (e.g., corporate mirror). Empty string uses the default https://registry.npmjs.org/. */
npmRegistryUrl: string;
}