Skip to content

Commit c4a43cb

Browse files
kwburns-konginsomnia-infra
authored andcommitted
feat: revamping pre/post scripting sandbox (#9794)
* feat: revamping pre/post scripting sandbox * feat: added UI setting to enable/disable specific checks (cherry picked from commit f220db3)
1 parent b460233 commit c4a43cb

18 files changed

Lines changed: 1847 additions & 142 deletions

package-lock.json

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/insomnia-smoke-test/tests/smoke/pre-request-script-features.test.ts

Lines changed: 178 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ test.describe('pre-request features tests', () => {
208208
}),
209209
};
210210
});
211+
211212
test('run test cases', async ({ page }) => {
212213
for (const tc of testCases) {
213214
console.log(`Running test case: ${tc.name}`);
@@ -230,6 +231,7 @@ test.describe('pre-request features tests', () => {
230231
tc.customVerify(bodyJson);
231232
}
232233
});
234+
233235
test('send request with content type', async ({ page }) => {
234236
await page.getByTestId('settings-button').click();
235237
await page.getByTestId('dataFolders').click();
@@ -660,14 +662,17 @@ test.describe('unhappy paths', () => {
660662
await page.getByTestId('request-pane').getByRole('button', { name: 'Send' }).click();
661663

662664
// verify
663-
await expect.soft(page.getByTestId('response-pane')).toContainText('my custom error');
665+
await expect
666+
.soft(page.getByTestId('response-pane'))
667+
.toContainText(`my custom error`);
664668

665669
await page.getByRole('tab', { name: 'Scripts' }).click();
666670
await page.getByTestId('CodeEditor').getByRole('textbox').press('ControlOrMeta+a');
667671
await page.keyboard.press('Backspace');
668672
await editor.fill(`insomnia.INVALID_FIELD.set('', '')`);
669673

670-
await page.getByRole('tab', { name: 'Body' }).click();
674+
// CodeMirror debounces onChange by DEBOUNCE_MILLIS (100ms).
675+
await page.evaluate(() => new Promise(resolve => setTimeout(resolve, 150)));
671676

672677
// send
673678
await page.getByTestId('request-pane').getByRole('button', { name: 'Send' }).click();
@@ -678,3 +683,174 @@ test.describe('unhappy paths', () => {
678683
.toContainText(`Cannot read properties of undefined (reading 'set')`);
679684
});
680685
});
686+
687+
test.describe('sandbox features', () => {
688+
test.slow(process.platform === 'darwin' || process.platform === 'win32', 'Slow app start on these platforms');
689+
690+
test.beforeEach(async ({ app, page }) => {
691+
const text = await loadFixture('pre-request-collection.yaml');
692+
await app.evaluate(async ({ clipboard }, text) => clipboard.writeText(text), text);
693+
694+
await page.getByLabel('Import').click();
695+
await page.locator('[data-test-id="import-from-clipboard"]').click();
696+
await page.getByRole('button', { name: 'Scan' }).click();
697+
await page.getByRole('dialog').getByRole('button', { name: 'Import' }).click();
698+
});
699+
700+
// Blocked Roots / Scopes group: 'this' is blocked.
701+
test('blocked roots / scopes group', async ({ page }) => {
702+
await page.getByLabel('Request Collection').getByTestId('echo pre-request script result').press('Enter');
703+
704+
705+
await page.getByRole('tab', { name: 'Scripts' }).click();
706+
const editor = page.getByTestId('CodeEditor').getByRole('textbox');
707+
708+
// enter script that accesses a property on 'this'.
709+
await editor.fill(`insomnia.environment.set('result', String(this?.process));`);
710+
711+
// CodeMirror debounces onChange by DEBOUNCE_MILLIS (100ms).
712+
await page.evaluate(() => new Promise(resolve => setTimeout(resolve, 150)));
713+
714+
await page.getByTestId('request-pane').getByRole('button', { name: 'Send' }).click();
715+
716+
// verify blocked-root error
717+
await expect
718+
.soft(page.getByTestId('response-pane'))
719+
.toContainText("The script was blocked because it used 'this'.");
720+
721+
// navigate to Settings → Scripting, disable the "Scopes" blocked roots group
722+
await page.getByTestId('settings-button').click();
723+
await page.locator('text=Insomnia Preferences').first().click();
724+
await page.getByRole('tab', { name: 'Scripting' }).click();
725+
const scopesSwitch = page.locator('div:has(> h4:has-text("Scopes")) label[data-react-aria-pressable]');
726+
await scopesSwitch.scrollIntoViewIfNeeded();
727+
await scopesSwitch.click();
728+
729+
await page.locator('.app').press('Escape');
730+
731+
// re-send — no sandbox error; this === undefined.
732+
await page.getByTestId('request-pane').getByRole('button', { name: 'Send' }).click();
733+
await expect
734+
.soft(page.getByTestId('response-pane'))
735+
.not.toContainText("The script was blocked because it used 'this'.");
736+
await expect
737+
.soft(page.locator('[data-testid="response-status-tag"]:visible'))
738+
.toContainText('200 OK');
739+
});
740+
741+
// Blocked Properties / Prototype Mutation group: 'prototype' is blocked.
742+
test('blocked properties / prototype mutation group', async ({ page }) => {
743+
await page.getByLabel('Request Collection').getByTestId('echo pre-request script result').press('Enter');
744+
745+
// enter script that accesses Object.prototype.
746+
await page.getByRole('tab', { name: 'Scripts' }).click();
747+
const editor = page.getByTestId('CodeEditor').getByRole('textbox');
748+
await editor.fill(`insomnia.environment.set('result', typeof Object.prototype.toString);`);
749+
750+
// CodeMirror debounces onChange by DEBOUNCE_MILLIS (100ms).
751+
await page.evaluate(() => new Promise(resolve => setTimeout(resolve, 150)));
752+
753+
await page.getByTestId('request-pane').getByRole('button', { name: 'Send' }).click();
754+
755+
// verify blocked-property error
756+
await expect
757+
.soft(page.getByTestId('response-pane'))
758+
.toContainText("The script was blocked because it used the property 'prototype'.");
759+
760+
// navigate to Settings → Scripting, disable the "Prototype Mutation" blocked properties group
761+
await page.getByTestId('settings-button').click();
762+
await page.locator('text=Insomnia Preferences').first().click();
763+
await page.getByRole('tab', { name: 'Scripting' }).click();
764+
const protoMutationSwitch = page.locator('div:has(> h4:has-text("Prototype Mutation")) label[data-react-aria-pressable]');
765+
await protoMutationSwitch.scrollIntoViewIfNeeded();
766+
await protoMutationSwitch.click();
767+
await expect.soft(protoMutationSwitch).not.toHaveAttribute('data-selected');
768+
await page.locator('.app').press('Escape');
769+
770+
// re-send — prototype access now allowed; Object.prototype.toString is a function
771+
await page.getByTestId('request-pane').getByRole('button', { name: 'Send' }).click();
772+
await expect
773+
.soft(page.getByTestId('response-pane'))
774+
.not.toContainText("The script was blocked because it used the property 'prototype'.");
775+
await expect
776+
.soft(page.locator('[data-testid="response-status-tag"]:visible'))
777+
.toContainText('200 OK');
778+
});
779+
780+
// Mask Rules / Runtime APIs group: 'Function' is masked to undefined at runtime.
781+
test('Mask Rules / Runtime APIs group.', async ({ page }) => {
782+
await page.getByLabel('Request Collection').getByTestId('echo pre-request script result').press('Enter');
783+
784+
// enter script that uses the Function constructor, only masked at runtime.
785+
await page.getByRole('tab', { name: 'Scripts' }).click();
786+
const editor = page.getByTestId('CodeEditor').getByRole('textbox');
787+
await editor.fill(`const f = new Function('return 42'); insomnia.environment.set('result', f());`);
788+
789+
// CodeMirror debounces onChange by DEBOUNCE_MILLIS (100ms).
790+
await page.evaluate(() => new Promise(resolve => setTimeout(resolve, 150)));
791+
792+
// send — Function masked to undefined → V8 uses the identifier name: "Function is not a constructor"
793+
await page.getByTestId('request-pane').getByRole('button', { name: 'Send' }).click();
794+
795+
await expect
796+
.soft(page.getByTestId('response-pane'))
797+
.toContainText('Function is not a constructor');
798+
799+
// navigate to Settings → Scripting, disable the "Runtime APIs" mask group
800+
await page.getByTestId('settings-button').click();
801+
await page.locator('text=Insomnia Preferences').first().click();
802+
await page.getByRole('tab', { name: 'Scripting' }).click();
803+
const runtimeApisSwitch = page.locator('div:has(> h4:has-text("Runtime APIs")) label[data-react-aria-pressable]');
804+
await runtimeApisSwitch.scrollIntoViewIfNeeded();
805+
await runtimeApisSwitch.click();
806+
await expect.soft(runtimeApisSwitch).not.toHaveAttribute('data-selected');
807+
await page.locator('.app').press('Escape');
808+
809+
// re-send — Function is now the real constructor; script returns 42
810+
await page.getByTestId('request-pane').getByRole('button', { name: 'Send' }).click();
811+
await expect
812+
.soft(page.getByTestId('response-pane'))
813+
.not.toContainText('Function is not a constructor');
814+
await expect
815+
.soft(page.locator('[data-testid="response-status-tag"]:visible'))
816+
.toContainText('200 OK');
817+
});
818+
819+
test('Layered security / unblocked properties resolve undefined', async ({ page }) => {
820+
await page.getByLabel('Request Collection').getByTestId('echo pre-request script result').press('Enter');
821+
822+
// enter script that accesses a property on 'process'.
823+
await page.getByRole('tab', { name: 'Scripts' }).click();
824+
const editor = page.getByTestId('CodeEditor').getByRole('textbox');
825+
await editor.fill(`insomnia.environment.set('result', String(process?.version));`);
826+
827+
// CodeMirror debounces onChange by DEBOUNCE_MILLIS (100ms).
828+
await page.evaluate(() => new Promise(resolve => setTimeout(resolve, 150)));
829+
830+
await page.getByTestId('request-pane').getByRole('button', { name: 'Send' }).click();
831+
832+
// verify blocked-root error
833+
await expect
834+
.soft(page.getByTestId('response-pane'))
835+
.toContainText("The script was blocked because it used 'process'.");
836+
837+
// navigate to Settings → Scripting, disable only the "Node.js Internals" BLOCKED ROOTS group.
838+
await page.getByTestId('settings-button').click();
839+
await page.locator('text=Insomnia Preferences').first().click();
840+
await page.getByRole('tab', { name: 'Scripting' }).click();
841+
const nodeInternalsSwitch = page.locator('xpath=//h4[normalize-space(text())="Node.js Internals"]/following-sibling::div[1]//label[@data-react-aria-pressable]');
842+
await nodeInternalsSwitch.scrollIntoViewIfNeeded();
843+
await nodeInternalsSwitch.click();
844+
await expect.soft(nodeInternalsSwitch).not.toHaveAttribute('data-selected');
845+
await page.locator('.app').press('Escape');
846+
847+
// process?.version === undefined.
848+
await page.getByTestId('request-pane').getByRole('button', { name: 'Send' }).click();
849+
await expect
850+
.soft(page.getByTestId('response-pane'))
851+
.not.toContainText("The script was blocked because it used 'process'.");
852+
await expect
853+
.soft(page.locator('[data-testid="response-status-tag"]:visible'))
854+
.toContainText('200 OK');
855+
});
856+
});

packages/insomnia/config/renderer-node-import-baseline.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,18 @@
9292
"importer": "src/script-executor.ts",
9393
"builtin": "fs/promises"
9494
},
95+
{
96+
"importer": "src/scripting/require-interceptor.ts",
97+
"builtin": "buffer"
98+
},
99+
{
100+
"importer": "src/scripting/require-interceptor.ts",
101+
"builtin": "timers"
102+
},
103+
{
104+
"importer": "src/scripting/require-interceptor.ts",
105+
"builtin": "util"
106+
},
95107
{
96108
"importer": "src/templating/base-extension.ts",
97109
"builtin": "crypto"

packages/insomnia/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@
7373
"@tailwindcss/typography": "^0.5.16",
7474
"@tanstack/react-virtual": "3.13.12",
7575
"@xmldom/xmldom": "^0.9.8",
76+
"acorn": "^8.16.0",
77+
"acorn-walk": "^8.3.5",
7678
"ajv": "^8.17.1",
7779
"apiconnect-wsdl": "2.0.36",
7880
"aws4": "^1.13.2",

packages/insomnia/src/common/settings.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,16 @@ export interface Settings {
163163
saveVaultKeyToOSSecretManager: boolean;
164164
vaultSecretCacheDuration: number;
165165
dataFolders: string[];
166+
// AST and shadowing check.
167+
scriptSandboxEnabled: boolean;
168+
// Wraps the user script in 'use strict', preventing accidental globals and making `this` undefined.
169+
scriptStrictModeEnabled: boolean;
170+
// Names of security rules that have been individually disabled.
171+
disabledSecurityRules: string[];
172+
// AST blocked-property names that have been individually disabled.
173+
disabledBlockedProperties: string[];
174+
// AST blocked-root names that have been individually disabled.
175+
disabledBlockedRoots: string[];
166176
/** Custom npm registry URL for plugin installation (e.g., corporate mirror). Empty string uses the default https://registry.npmjs.org/. */
167177
npmRegistryUrl: string;
168178
}

packages/insomnia/src/entry.hidden-window-preload.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
stopMonitorAsyncTasks,
1414
} from '../../insomnia-scripting-environment/src/objects';
1515
// this will also import lots of node_modules into the preload script, consider moving this file insomnia-scripting-environment
16-
import { requireInterceptor } from './require-interceptor';
16+
import { requireInterceptor } from './scripting/require-interceptor';
1717

1818
export interface HiddenBrowserWindowToMainBridgeAPI {
1919
requireInterceptor: (module: string) => any;

0 commit comments

Comments
 (0)