Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion packages/insomnia/src/templating/sandbox/PERMISSIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,18 @@ by Insomnia (a pure-JS reimplementation or a host-backed shim), never the raw No

- **Baseline (no manifest needed):** `path`, `crypto`.
- **Grantable:** any other module in the sandbox registry. Declaring one adds it to your grant.
- Pure-JS reimplementations: `events` (and more via M2).
- Pure-JS reimplementations: `events`, `util` (and more via M2).
- `util` implements `format`, `promisify` (including a `Symbol.for("nodejs.util.promisify.custom")`
override and a `.custom` property), and `types.is*` (`isDate`/`isRegExp`/`isPromise`/`isMap`/
`isSet`/`isWeakMap`/`isWeakSet`/`isArrayBuffer`/`isDataView`/`isTypedArray`/`isNativeError`/
`isBooleanObject`/`isNumberObject`/`isStringObject`/`isAsyncFunction`/`isGeneratorFunction`) —
verified against `node:util` for all of `format`'s specifiers (`%s %d %i %f %j %o %O %c %%`),
its `-0`/`NaN`/`bigint`/`symbol` coercion quirks, quote-character selection for inspected
strings, and `promisify`'s error/multi-value/custom-override semantics. Two deliberate,
documented gaps: `util.inspect`/`inherits`/`deprecate` are not implemented at all (absent from
the exports object, so calling them throws a plain "not a function" TypeError); and `%o` is not
distinguished from `%O` — real Node's `%o` additionally reveals non-enumerable properties (e.g.
an array's `.length`) and inspects to depth 4, neither of which this module replicates.
- **Vetted npm libraries** (pinned + pre-bundled by Insomnia): `uuid`, `ajv`. These are real
libraries bundled to run inside the sandbox; they're only loaded when a plugin declares them.
Each is sourced from an isolated, exact-pinned install at
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,45 @@ exports[`sandbox surface > matches surface snapshot 1`] = `
"require("events"): object",
"require("events").EventEmitter: function(0)",
"require("events").EventEmitter.prototype: object",
"require("util"): object",
"require("util").format: function(0)",
"require("util").format.prototype: object",
"require("util").promisify: function(1)",
"require("util").promisify.custom: symbol",
"require("util").promisify.prototype: object",
"require("util").types: object",
"require("util").types.isArrayBuffer: function(1)",
"require("util").types.isArrayBuffer.prototype: object",
"require("util").types.isAsyncFunction: function(1)",
"require("util").types.isAsyncFunction.prototype: object",
"require("util").types.isBooleanObject: function(1)",
"require("util").types.isBooleanObject.prototype: object",
"require("util").types.isDataView: function(1)",
"require("util").types.isDataView.prototype: object",
"require("util").types.isDate: function(1)",
"require("util").types.isDate.prototype: object",
"require("util").types.isGeneratorFunction: function(1)",
"require("util").types.isGeneratorFunction.prototype: object",
"require("util").types.isMap: function(1)",
"require("util").types.isMap.prototype: object",
"require("util").types.isNativeError: function(1)",
"require("util").types.isNativeError.prototype: object",
"require("util").types.isNumberObject: function(1)",
"require("util").types.isNumberObject.prototype: object",
"require("util").types.isPromise: function(1)",
"require("util").types.isPromise.prototype: object",
"require("util").types.isRegExp: function(1)",
"require("util").types.isRegExp.prototype: object",
"require("util").types.isSet: function(1)",
"require("util").types.isSet.prototype: object",
"require("util").types.isStringObject: function(1)",
"require("util").types.isStringObject.prototype: object",
"require("util").types.isTypedArray: function(1)",
"require("util").types.isTypedArray.prototype: object",
"require("util").types.isWeakMap: function(1)",
"require("util").types.isWeakMap.prototype: object",
"require("util").types.isWeakSet: function(1)",
"require("util").types.isWeakSet.prototype: object",
"require("uuid"): object",
"require("uuid").MAX: string",
"require("uuid").NIL: string",
Expand Down
171 changes: 171 additions & 0 deletions packages/insomnia/src/templating/sandbox/module-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,11 +101,182 @@ const EVENTS_FACTORY = [
'}',
].join('\n');

// Reduced, documented replacement for node:util — only `format`, `promisify`, and `types.is*` are
// ported (PERMISSIONS.md records the exclusions: no `inspect`/`inherits`/`deprecate`, and %o is not
// distinguished from %O — no showHidden/proxy/unbounded-depth inspection). `format`'s object/array
// rendering and quote-character selection were verified line-for-line against real node:util's
// output (including the %s-vs-%O/extra-arg divergence in how each treats strings and functions, and
// the -0/bigint/symbol coercion quirks of %d/%i/%f) before transcription here.
const UTIL_FACTORY = [
'function () {',
' function isValidIdentifierKey(k) { return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(k); }',
' function quoteString(s) {',
' if (s.indexOf("\'") === -1) { return "\'" + s + "\'"; }',
' if (s.indexOf(\'"\') === -1) { return \'"\' + s + \'"\'; }',
' if (s.indexOf("`") === -1) { return "`" + s + "`"; }',
' var bs = String.fromCharCode(92);',
' return "\'" + s.split("\'").join(bs + "\'") + "\'";',
' }',
' function formatKey(k) { return isValidIdentifierKey(k) ? k : quoteString(k); }',
' function formatNumber(n) {',
' if (n === 0 && 1 / n === -Infinity) { return "-0"; }',
' return String(n);',
' }',
' function formatPrimitiveNonString(v) {',
' if (v === undefined) { return "undefined"; }',
' var t = typeof v;',
' if (t === "boolean") { return v ? "true" : "false"; }',
' if (t === "bigint") { return String(v) + "n"; }',
' if (t === "number") { return formatNumber(v); }',
' return String(v);',
' }',
' function inspect(v, maxDepth) {',
' if (v === null) { return "null"; }',
' var t = typeof v;',
' if (t === "string") { return quoteString(v); }',
' if (t === "function") { var nm = v.name; return nm ? "[Function: " + nm + "]" : "[Function (anonymous)]"; }',
' if (t !== "object") { return formatPrimitiveNonString(v); }',
' return inspectContainer(v, 0, maxDepth);',
' }',
' function inspectContainer(v, depth, maxDepth) {',
' if (depth > maxDepth) { return Array.isArray(v) ? "[Array]" : "[Object]"; }',
' if (Array.isArray(v)) {',
' if (v.length === 0) { return "[]"; }',
' var parts = [];',
' for (var i = 0; i < v.length; i++) { parts.push(inspectNested(v[i], depth + 1, maxDepth)); }',
' return "[ " + parts.join(", ") + " ]";',
' }',
' var keys = Object.keys(v);',
' if (keys.length === 0) { return "{}"; }',
' var oparts = [];',
' for (var j = 0; j < keys.length; j++) {',
' var key = keys[j];',
' oparts.push(formatKey(key) + ": " + inspectNested(v[key], depth + 1, maxDepth));',
' }',
' return "{ " + oparts.join(", ") + " }";',
' }',
' function inspectNested(v, depth, maxDepth) {',
' if (v === null) { return "null"; }',
' var t = typeof v;',
' if (t === "string") { return quoteString(v); }',
' if (t === "function") { var nm = v.name; return nm ? "[Function: " + nm + "]" : "[Function (anonymous)]"; }',
' if (t !== "object") { return formatPrimitiveNonString(v); }',
' return inspectContainer(v, depth, maxDepth);',
' }',
' function formatS(v) {',
' if (typeof v === "bigint") { return String(v) + "n"; }',
' if (typeof v === "number") { return formatNumber(v); }',
' if (typeof v !== "object" || v === null) { return String(v); }',
' return inspect(v, 0);',
' }',
' function formatFull(v) { return inspect(v, 2); }',
' function formatJoin(v) { return typeof v === "string" ? v : formatFull(v); }',
' function fmtD(v) {',
' if (typeof v === "bigint") { return String(v) + "n"; }',
' if (typeof v === "symbol") { return "NaN"; }',
' return formatNumber(Number(v));',
' }',
' function fmtI(v) {',
' if (typeof v === "bigint") { return String(v) + "n"; }',
' if (typeof v === "symbol") { return "NaN"; }',
' return formatNumber(parseInt(v, 10));',
' }',
' function fmtF(v) {',
' if (typeof v === "symbol") { return "NaN"; }',
' return formatNumber(parseFloat(v));',
' }',
' function fmtJ(v) {',
' try {',
' var s = JSON.stringify(v);',
' return s === undefined ? "undefined" : s;',
' } catch (e) {',
' if (e && typeof e.message === "string" && e.message.indexOf("circular") !== -1) { return "[Circular]"; }',
' throw e;',
' }',
' }',
' function format() {',
' var args = Array.prototype.slice.call(arguments);',
' if (args.length === 0) { return ""; }',
' var first = args[0];',
' if (typeof first !== "string") {',
' var parts0 = [];',
' for (var k = 0; k < args.length; k++) { parts0.push(formatJoin(args[k])); }',
' return parts0.join(" ");',
' }',
' if (args.length === 1) { return first; }',
' var out = "";',
' var i = 0;',
' var argIndex = 1;',
' while (i < first.length) {',
' var ch = first.charAt(i);',
' if (ch === "%" && i + 1 < first.length) {',
' var spec = first.charAt(i + 1);',
' if (spec === "%") { out += "%"; i += 2; continue; }',
' if ("sdifjoOc".indexOf(spec) !== -1 && argIndex < args.length) {',
' var val = args[argIndex];',
' argIndex += 1;',
' if (spec === "s") { out += formatS(val); }',
' else if (spec === "d") { out += fmtD(val); }',
' else if (spec === "i") { out += fmtI(val); }',
' else if (spec === "f") { out += fmtF(val); }',
' else if (spec === "j") { out += fmtJ(val); }',
' else if (spec === "o" || spec === "O") { out += formatFull(val); }',
' i += 2;',
' continue;',
' }',
' }',
' out += ch;',
' i += 1;',
' }',
' for (; argIndex < args.length; argIndex++) { out += " " + formatJoin(args[argIndex]); }',
' return out;',
' }',
' var PROMISIFY_CUSTOM = Symbol.for("nodejs.util.promisify.custom");',
' function promisify(original) {',
' if (typeof original !== "function") { throw new TypeError("The \\"original\\" argument must be of type function"); }',
' if (original[PROMISIFY_CUSTOM]) { return original[PROMISIFY_CUSTOM]; }',
' function fn() {',
' var args = Array.prototype.slice.call(arguments);',
' var self = this;',
' return new Promise(function (resolve, reject) {',
' args.push(function (err) {',
' if (err) { reject(err); return; }',
' resolve(arguments.length > 1 ? arguments[1] : undefined);',
' });',
' original.apply(self, args);',
' });',
' }',
' return fn;',
' }',
' promisify.custom = PROMISIFY_CUSTOM;',
' var types = {',
' isDate: function (v) { return v instanceof Date; },',
' isRegExp: function (v) { return v instanceof RegExp; },',
' isPromise: function (v) { return v instanceof Promise; },',
' isMap: function (v) { return v instanceof Map; },',
' isSet: function (v) { return v instanceof Set; },',
' isWeakMap: function (v) { return v instanceof WeakMap; },',
' isWeakSet: function (v) { return v instanceof WeakSet; },',
' isArrayBuffer: function (v) { return v instanceof ArrayBuffer; },',
' isDataView: function (v) { return v instanceof DataView; },',
' isTypedArray: function (v) { return ArrayBuffer.isView(v) && !(v instanceof DataView); },',
' isNativeError: function (v) { return v instanceof Error; },',
' isBooleanObject: function (v) { return typeof v === "object" && v instanceof Boolean; },',
' isNumberObject: function (v) { return typeof v === "object" && v instanceof Number; },',
' isStringObject: function (v) { return typeof v === "object" && v instanceof String; },',
' isAsyncFunction: function (v) { return Object.prototype.toString.call(v) === "[object AsyncFunction]"; },',
' isGeneratorFunction: function (v) { return Object.prototype.toString.call(v) === "[object GeneratorFunction]"; }',
' };',
' return { format: format, promisify: promisify, types: types };',
'}',
].join('\n');

/** Every module the sandbox can serve. Grown deliberately, one vetted entry at a time (M2/M3). */
export const SANDBOX_MODULES: SandboxModuleDefinition[] = [
{ name: 'path', aliases: ['node:path'], factorySource: PATH_FACTORY },
{ name: 'crypto', aliases: ['node:crypto'], factorySource: CRYPTO_FACTORY },
{ name: 'events', aliases: ['node:events'], factorySource: EVENTS_FACTORY },
{ name: 'util', aliases: ['node:util'], factorySource: UTIL_FACTORY },
// Vetted npm libraries (M3), bundled + pinned by scripts/generate-sandbox-vendored.ts. Heavy, so
// only included in the eval'd registry source when a plugin declares them.
{ name: 'uuid', factorySource: UUID_FACTORY_SOURCE, heavy: true },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,40 @@ describe('manifest-declared module grants (C3)', () => {
}),
).rejects.toThrow("Module 'events' not permitted by manifest");
});

const utilTag =
"module.exports.templateTags = [{ name: 'r', run: function () { return require('util').format('%s is %d', 'answer', 42); } }];";

it('a plugin declaring the node:util alias can use format', async () => {
const actual = await runTagInSandbox({
pluginSource: utilTag,
tagName: 'r',
envelope: envelope([], resolveTemplateTagModules(['node:util'])),
bridge: noBridge,
});
expect(actual).toBe('answer is 42');
});

it('a plugin granted "util" can use format', async () => {
const actual = await runTagInSandbox({
pluginSource: utilTag,
tagName: 'r',
envelope: envelope([], resolveTemplateTagModules(['util'])),
bridge: noBridge,
});
expect(actual).toBe('answer is 42');
});

it('a plugin without the grant is denied "util" with the manifest message', async () => {
await expect(
runTagInSandbox({
pluginSource: utilTag,
tagName: 'r',
envelope: envelope([], resolveTemplateTagModules()),
bridge: noBridge,
}),
).rejects.toThrow("Module 'util' not permitted by manifest");
});
});

describe('ambient globals — sandbox stdlib (M2)', () => {
Expand Down
Loading
Loading