chore!: remove integration scripts' "Compatible Sandbox" (vm2)

pull/33628/head
Pierre 1 year ago committed by Guilherme Gazzo
parent d6b8fc6f10
commit 3ea02d3cc1
  1. 7
      .changeset/sharp-forks-give.md
  2. 7
      apps/meteor/app/integrations/server/api/api.js
  3. 7
      apps/meteor/app/integrations/server/lib/triggerHandler.js
  4. 12
      apps/meteor/app/integrations/server/lib/validateScriptEngine.ts
  5. 88
      apps/meteor/app/integrations/server/lib/vm2/buildSandbox.ts
  6. 111
      apps/meteor/app/integrations/server/lib/vm2/vm2.ts
  7. 4
      apps/meteor/app/integrations/server/methods/incoming/updateIncomingIntegration.ts
  8. 4
      apps/meteor/app/integrations/server/methods/outgoing/updateOutgoingIntegration.ts
  9. 8
      apps/meteor/client/views/admin/integrations/incoming/IncomingWebhookForm.tsx
  10. 8
      apps/meteor/client/views/admin/integrations/outgoing/OutgoingWebhookForm.tsx
  11. 1
      apps/meteor/package.json
  12. 2
      packages/core-typings/src/IIntegration.ts
  13. 3
      packages/i18n/src/locales/en.i18n.json
  14. 2
      packages/i18n/src/locales/hi-IN.i18n.json
  15. 4
      packages/i18n/src/locales/se.i18n.json
  16. 4
      packages/rest-typings/src/v1/integrations/IntegrationsUpdateProps.ts
  17. 17
      yarn.lock

@ -0,0 +1,7 @@
---
'@rocket.chat/meteor': major
'@rocket.chat/core-typings': patch
'@rocket.chat/rest-typings': patch
---
Removed the deprecated "Compatible Sandbox" option from integration scripts and the dependencies that this sandbox mode relied on.

@ -6,16 +6,15 @@ import { API, APIClass, defaultRateLimiterOptions } from '../../../api/server';
import { processWebhookMessage } from '../../../lib/server/functions/processWebhookMessage';
import { settings } from '../../../settings/server';
import { IsolatedVMScriptEngine } from '../lib/isolated-vm/isolated-vm';
import { VM2ScriptEngine } from '../lib/vm2/vm2';
import { incomingLogger } from '../logger';
import { addOutgoingIntegration } from '../methods/outgoing/addOutgoingIntegration';
import { deleteOutgoingIntegration } from '../methods/outgoing/deleteOutgoingIntegration';
const vm2Engine = new VM2ScriptEngine(true);
const ivmEngine = new IsolatedVMScriptEngine(true);
function getEngine(integration) {
return integration.scriptEngine === 'isolated-vm' ? ivmEngine : vm2Engine;
// eslint-disable-next-line no-unused-vars
function getEngine(_integration) {
return ivmEngine;
}
async function createIntegration(options, user) {

@ -12,14 +12,12 @@ import { outgoingEvents } from '../../lib/outgoingEvents';
import { outgoingLogger } from '../logger';
import { IsolatedVMScriptEngine } from './isolated-vm/isolated-vm';
import { updateHistory } from './updateHistory';
import { VM2ScriptEngine } from './vm2/vm2';
class RocketChatIntegrationHandler {
constructor() {
this.successResults = [200, 201, 202];
this.compiledScripts = {};
this.triggers = {};
this.vm2Engine = new VM2ScriptEngine(false);
this.ivmEngine = new IsolatedVMScriptEngine(false);
}
@ -47,8 +45,9 @@ class RocketChatIntegrationHandler {
}
}
getEngine(integration) {
return integration.scriptEngine === 'isolated-vm' ? this.ivmEngine : this.vm2Engine;
// eslint-disable-next-line no-unused-vars
getEngine(_integration) {
return this.ivmEngine;
}
removeIntegration(record) {

@ -9,14 +9,14 @@ export const validateScriptEngine = (engine?: IntegrationScriptEngine) => {
throw new Error('integration-scripts-disabled');
}
const engineCode = engine === 'isolated-vm' ? 'ivm' : 'vm2';
if (engine && engine !== 'isolated-vm') {
throw new Error('integration-scripts-unknown-engine');
}
if (engineCode === FREEZE_INTEGRATION_SCRIPTS_VALUE) {
if (engineCode === 'ivm') {
throw new Error('integration-scripts-isolated-vm-disabled');
}
const engineCode = 'ivm';
throw new Error('integration-scripts-vm2-disabled');
if (engineCode === FREEZE_INTEGRATION_SCRIPTS_VALUE) {
throw new Error('integration-scripts-isolated-vm-disabled');
}
return true;

@ -1,88 +0,0 @@
import * as Models from '@rocket.chat/models';
import moment from 'moment';
import _ from 'underscore';
import * as s from '../../../../../lib/utils/stringUtils';
import { deasyncPromise } from '../../../../../server/deasync/deasync';
import { httpCall } from '../../../../../server/lib/http/call';
const forbiddenModelMethods: readonly (keyof typeof Models)[] = ['registerModel', 'getCollectionName'];
type ModelName = Exclude<keyof typeof Models, (typeof forbiddenModelMethods)[number]>;
export type Vm2Sandbox<IsIncoming extends boolean> = {
scriptTimeout: (reject: (reason?: any) => void) => ReturnType<typeof setTimeout>;
_: typeof _;
s: typeof s;
console: typeof console;
moment: typeof moment;
Promise: typeof Promise;
Store: {
set: IsIncoming extends true ? (key: string, value: any) => any : (key: string, value: any) => void;
get: (key: string) => any;
};
HTTP: (method: string, url: string, options: Record<string, any>) => unknown;
} & (IsIncoming extends true ? { Livechat: undefined } : never) &
Record<ModelName, (typeof Models)[ModelName]>;
export const buildSandbox = <IsIncoming extends boolean>(
store: Record<string, any>,
isIncoming?: IsIncoming,
): {
store: Record<string, any>;
sandbox: Vm2Sandbox<IsIncoming>;
} => {
const httpAsync = async (method: string, url: string, options: Record<string, any>) => {
try {
return {
result: await httpCall(method, url, options),
};
} catch (error) {
return { error };
}
};
const sandbox = {
scriptTimeout(reject: (reason?: any) => void) {
return setTimeout(() => reject('timed out'), 3000);
},
_,
s,
console,
moment,
Promise,
// There's a small difference between the sandbox that is sent to incoming and to outgoing scripts
// Technically we could unify this but since we're deprecating vm2 anyway I'm keeping this old behavior here until the feature is removed completely
...(isIncoming
? {
Livechat: undefined,
Store: {
set: (key: string, val: any): any => {
store[key] = val;
return val;
},
get: (key: string) => store[key],
},
}
: {
Store: {
set: (key: string, val: any): void => {
store[key] = val;
},
get: (key: string) => store[key],
},
}),
HTTP: (method: string, url: string, options: Record<string, any>) => {
// TODO: deprecate, track and alert
return deasyncPromise(httpAsync(method, url, options));
},
} as Vm2Sandbox<IsIncoming>;
(Object.keys(Models) as ModelName[])
.filter((k) => !forbiddenModelMethods.includes(k))
.forEach((k) => {
sandbox[k] = Models[k];
});
return { store, sandbox };
};

@ -1,111 +0,0 @@
import type { IIntegration } from '@rocket.chat/core-typings';
import { VM, VMScript } from 'vm2';
import { IntegrationScriptEngine } from '../ScriptEngine';
import type { IScriptClass } from '../definition';
import { buildSandbox, type Vm2Sandbox } from './buildSandbox';
const DISABLE_INTEGRATION_SCRIPTS = ['yes', 'true', 'vm2'].includes(String(process.env.DISABLE_INTEGRATION_SCRIPTS).toLowerCase());
export class VM2ScriptEngine<IsIncoming extends boolean> extends IntegrationScriptEngine<IsIncoming> {
protected isDisabled(): boolean {
return DISABLE_INTEGRATION_SCRIPTS;
}
protected buildSandbox(store: Record<string, any> = {}): { store: Record<string, any>; sandbox: Vm2Sandbox<IsIncoming> } {
return buildSandbox<IsIncoming>(store, this.incoming);
}
protected async runScriptMethod({
integrationId,
script,
method,
params,
}: {
integrationId: IIntegration['_id'];
script: IScriptClass;
method: keyof IScriptClass;
params: Record<string, any>;
}): Promise<any> {
const { sandbox } = this.buildSandbox(this.compiledScripts[integrationId].store);
const vm = new VM({
timeout: 3000,
sandbox: {
...sandbox,
script,
method,
params,
...(this.incoming && 'request' in params ? { request: params.request } : {}),
},
});
return new Promise((resolve, reject) => {
process.nextTick(async () => {
try {
const scriptResult = await vm.run(`
new Promise((resolve, reject) => {
scriptTimeout(reject);
try {
resolve(script[method](params))
} catch(e) {
reject(e);
}
}).catch((error) => { throw new Error(error); });
`);
resolve(scriptResult);
} catch (e) {
reject(e);
}
});
});
}
protected async getIntegrationScript(integration: IIntegration): Promise<Partial<IScriptClass>> {
if (this.disabled) {
throw new Error('integration-scripts-disabled');
}
const compiledScript = this.compiledScripts[integration._id];
if (compiledScript && +compiledScript._updatedAt === +integration._updatedAt) {
return compiledScript.script;
}
const script = integration.scriptCompiled;
const { store, sandbox } = this.buildSandbox();
try {
this.logger.info({ msg: 'Will evaluate script of Trigger', integration: integration.name });
this.logger.debug(script);
const vmScript = new VMScript(`${script}; Script;`, 'script.js');
const vm = new VM({
sandbox,
});
const ScriptClass = vm.run(vmScript);
if (ScriptClass) {
this.compiledScripts[integration._id] = {
script: new ScriptClass(),
store,
_updatedAt: integration._updatedAt,
};
return this.compiledScripts[integration._id].script;
}
} catch (err) {
this.logger.error({
msg: 'Error evaluating Script in Trigger',
integration: integration.name,
script,
err,
});
throw new Error('error-evaluating-script');
}
this.logger.error({ msg: 'Class "Script" not in Trigger', integration: integration.name });
throw new Error('class-script-not-found');
}
}

@ -67,8 +67,8 @@ Meteor.methods<ServerMethods>({
});
}
const oldScriptEngine = currentIntegration.scriptEngine ?? 'vm2';
const scriptEngine = integration.scriptEngine ?? oldScriptEngine;
const oldScriptEngine = currentIntegration.scriptEngine;
const scriptEngine = integration.scriptEngine ?? oldScriptEngine ?? 'isolated-vm';
if (
integration.script?.trim() &&
(scriptEngine !== oldScriptEngine || integration.script?.trim() !== currentIntegration.script?.trim())

@ -54,8 +54,8 @@ Meteor.methods<ServerMethods>({
throw new Meteor.Error('invalid_integration', '[methods] updateOutgoingIntegration -> integration not found');
}
const oldScriptEngine = currentIntegration.scriptEngine ?? 'vm2';
const scriptEngine = integration.scriptEngine ?? oldScriptEngine;
const oldScriptEngine = currentIntegration.scriptEngine;
const scriptEngine = integration.scriptEngine ?? oldScriptEngine ?? 'isolated-vm';
if (
integration.script?.trim() &&
(scriptEngine !== oldScriptEngine || integration.script?.trim() !== currentIntegration.script?.trim())

@ -57,13 +57,7 @@ const IncomingWebhookForm = ({ webhookData }: { webhookData?: Serialized<IIncomi
const { copy: copyToken } = useClipboardWithToast(`${webhookData?._id}/${webhookData?.token}`);
const { copy: copyCurlData } = useClipboardWithToast(curlData);
const scriptEngineOptions: SelectOption[] = useMemo(
() => [
['vm2', t('Script_Engine_vm2')],
['isolated-vm', t('Script_Engine_isolated_vm')],
],
[t],
);
const scriptEngineOptions: SelectOption[] = useMemo(() => [['isolated-vm', t('Script_Engine_isolated_vm')]], [t]);
const hilightedExampleJson = useHighlightedCode('json', JSON.stringify(exampleData, null, 2));

@ -75,13 +75,7 @@ const OutgoingWebhookForm = () => {
[t],
);
const scriptEngineOptions: SelectOption[] = useMemo(
() => [
['vm2', t('Script_Engine_vm2')],
['isolated-vm', t('Script_Engine_isolated_vm')],
],
[t],
);
const scriptEngineOptions: SelectOption[] = useMemo(() => [['isolated-vm', t('Script_Engine_isolated_vm')]], [t]);
const showChannel = useMemo(() => outgoingEvents[event].use.channel, [event]);
const showTriggerWords = useMemo(() => outgoingEvents[event].use.triggerWords, [event]);

@ -439,7 +439,6 @@
"use-subscription": "~1.6.0",
"use-sync-external-store": "^1.2.2",
"uuid": "^8.3.2",
"vm2": "^3.9.19",
"webdav": "^4.11.4",
"xml-crypto": "~3.1.0",
"xml-encryption": "~3.0.2",

@ -1,7 +1,7 @@
import type { IRocketChatRecord } from './IRocketChatRecord';
import type { IUser } from './IUser';
export type IntegrationScriptEngine = 'vm2' | 'isolated-vm';
export type IntegrationScriptEngine = 'isolated-vm';
export interface IIncomingIntegration extends IRocketChatRecord {
type: 'webhook-incoming';

@ -2787,8 +2787,8 @@
"Integration_Incoming_WebHook": "Incoming WebHook Integration",
"Integration_New": "New Integration",
"integration-scripts-disabled": "Integration Scripts are Disabled",
"integration-scripts-unknown-engine": "Unknown Integration Script Engine",
"integration-scripts-isolated-vm-disabled": "The \"Secure Sandbox\" may not be used on new or modified scripts.",
"integration-scripts-vm2-disabled": "The \"Compatible Sandbox\" may not be used on new or modified scripts.",
"Integration_Outgoing_WebHook": "Outgoing WebHook Integration",
"Integration_Outgoing_WebHook_History": "Outgoing WebHook Integration History",
"Integration_Outgoing_WebHook_History_Data_Passed_To_Trigger": "Data Passed to Integration",
@ -4812,7 +4812,6 @@
"Script_Enabled": "Script Enabled",
"Script_Engine": "Script Sandbox",
"Script_Engine_Description": "Older scripts may require the compatible sandbox to run properly, but all new scripts should try to use the secure sandbox instead.",
"Script_Engine_vm2": "Compatible Sandbox (Deprecated)",
"Script_Engine_isolated_vm": "Secure Sandbox",
"Search": "Search",
"Searchable": "Searchable",

@ -2615,7 +2615,6 @@
"Integration_New": "नय एककरण",
"integration-scripts-disabled": "एककरण सिट अकषम ह",
"integration-scripts-isolated-vm-disabled": "\"सिर सडबस\" क उपयग नई यित सिट पर नहि सकत।",
"integration-scripts-vm2-disabled": "\"सगत सडबस\" क उपयग नई यित सिट पर नहि सकत।",
"Integration_Outgoing_WebHook": "आउटगग वबहक एककरण",
"Integration_Outgoing_WebHook_History": "आउटगग वबहक एककरण इतिस",
"Integration_Outgoing_WebHook_History_Data_Passed_To_Trigger": "ड एककरण किए पित कि गय",
@ -4504,7 +4503,6 @@
"Script_Enabled": "सिट सकषम",
"Script_Engine": "सिट सडबस",
"Script_Engine_Description": "पिट कक स चलिए सगत सडबस क आवशयकत सकत, लिन सभ नई सिट क इसक बजय सरकित सडबस क उपयग करनरयस करनिए।",
"Script_Engine_vm2": "सगत सडबस (असत)",
"Script_Engine_isolated_vm": "सरकित सडबस",
"Search": "खज",
"Searchable": "खज सकन",

@ -2782,9 +2782,6 @@
"Integration_New": "New Integration",
"integration-scripts-disabled": "Integration Scripts are Disabled",
"integration-scripts-isolated-vm-disabled": "The \"Secure Sandbox\" may not be used on new or modified scripts.",
"integration-scripts-vm2-disabled": "The \"Compatible Sandbox\" may not be used on new or modified scripts.",
"Integration_Outgoing_WebHook": "Outgoing WebHook Integration",
"Integration_Outgoing_WebHook_History": "Outgoing WebHook Integration History",
"Integration_Outgoing_WebHook_History_Data_Passed_To_Trigger": "Data Passed to Integration",
"Integration_Outgoing_WebHook_History_Data_Passed_To_URL": "Data Passed to URL",
"Integration_Outgoing_WebHook_History_Error_Stacktrace": "Error Stacktrace",
@ -4804,7 +4801,6 @@
"Script_Enabled": "Script Enabled",
"Script_Engine": "Script Sandbox",
"Script_Engine_Description": "Older scripts may require the compatible sandbox to run properly, but all new scripts should try to use the secure sandbox instead.",
"Script_Engine_vm2": "Compatible Sandbox (Deprecated)",
"Script_Engine_isolated_vm": "Secure Sandbox",
"Search": "Search",
"Searchable": "Searchable",

@ -9,7 +9,7 @@ export type IntegrationsUpdateProps =
integrationId: string;
channel: string;
scriptEnabled: boolean;
scriptEngine: 'isolated-vm' | 'vm2';
scriptEngine: 'isolated-vm';
overrideDestinationChannelEnabled?: boolean;
script?: string;
name: string;
@ -33,7 +33,7 @@ export type IntegrationsUpdateProps =
token?: string;
scriptEnabled: boolean;
scriptEngine: 'isolated-vm' | 'vm2';
scriptEngine: 'isolated-vm';
script?: string;
runOnEdits?: boolean;

@ -9882,7 +9882,6 @@ __metadata:
use-subscription: "npm:~1.6.0"
use-sync-external-store: "npm:^1.2.2"
uuid: "npm:^8.3.2"
vm2: "npm:^3.9.19"
webdav: "npm:^4.11.4"
xml-crypto: "npm:~3.1.0"
xml-encryption: "npm:~3.0.2"
@ -16116,7 +16115,7 @@ __metadata:
languageName: node
linkType: hard
"acorn-walk@npm:^8.0.0, acorn-walk@npm:^8.0.2, acorn-walk@npm:^8.1.1, acorn-walk@npm:^8.2.0":
"acorn-walk@npm:^8.0.0, acorn-walk@npm:^8.0.2, acorn-walk@npm:^8.1.1":
version: 8.3.2
resolution: "acorn-walk@npm:8.3.2"
checksum: 10/57dbe2fd8cf744f562431775741c5c087196cd7a65ce4ccb3f3981cdfad25cd24ad2bad404997b88464ac01e789a0a61e5e355b2a84876f13deef39fb39686ca
@ -16141,7 +16140,7 @@ __metadata:
languageName: node
linkType: hard
"acorn@npm:^8.0.4, acorn@npm:^8.1.0, acorn@npm:^8.2.4, acorn@npm:^8.4.1, acorn@npm:^8.5.0, acorn@npm:^8.7.0, acorn@npm:^8.7.1, acorn@npm:^8.8.1, acorn@npm:^8.8.2, acorn@npm:^8.9.0":
"acorn@npm:^8.0.4, acorn@npm:^8.1.0, acorn@npm:^8.2.4, acorn@npm:^8.4.1, acorn@npm:^8.5.0, acorn@npm:^8.7.1, acorn@npm:^8.8.1, acorn@npm:^8.8.2, acorn@npm:^8.9.0":
version: 8.11.3
resolution: "acorn@npm:8.11.3"
bin:
@ -42789,18 +42788,6 @@ __metadata:
languageName: node
linkType: hard
"vm2@npm:^3.9.19":
version: 3.9.19
resolution: "vm2@npm:3.9.19"
dependencies:
acorn: "npm:^8.7.0"
acorn-walk: "npm:^8.2.0"
bin:
vm2: bin/vm2
checksum: 10/8526737abbfb0ce61bae3d0ffe9ecc96eb093c859c933ed4d9cf663c7663da8f73a733771b9b21251c4d82a28a0600a0fcfd0044b685686eeef7a75e632294e6
languageName: node
linkType: hard
"void-elements@npm:3.1.0":
version: 3.1.0
resolution: "void-elements@npm:3.1.0"

Loading…
Cancel
Save