Skip to content

Commit 0ceb2b2

Browse files
committed
Updated Ecma sandbox connector to validate main function existance in code
1 parent 8418450 commit 0ceb2b2

File tree

3 files changed

+83
-32
lines changed

3 files changed

+83
-32
lines changed

packages/core/src/helpers/ECMASandbox.helper.ts

Lines changed: 43 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ async function setupIsolate() {
3030
const context = await isolate.createContext();
3131
const jail = context.global;
3232
await jail.set('global', jail.derefInto());
33-
3433
// Define a SafeBuffer object
3534
const ___internal = {
3635
b64decode: (str) => Buffer.from(str, 'base64').toString('utf8'),
@@ -62,45 +61,66 @@ export async function runJs(code: string) {
6261

6362
if (!code.endsWith(';')) code += ';';
6463

65-
let scriptCode = '';
6664
const { isolate, context, jail } = await setupIsolate();
6765
const remoteUrls = await extractFetchUrls(code);
6866
for (const url of remoteUrls) {
6967
const remoteCode = await fetchCodeFromCDN(url);
70-
context.eval(`${remoteCode}`);
68+
await context.eval(`${remoteCode}`);
7169
}
72-
const randomId = Math.random().toString(36).substring(2, 15);
73-
const resId = `res${randomId}`;
74-
scriptCode = `
75-
var ${resId};
76-
${code};
77-
78-
${resId} = JSON.stringify(_output);
79-
${resId};
80-
`;
81-
const script: any = await isolate.compileScript(scriptCode).catch((err) => {
70+
71+
const executionCode = `
72+
(async () => {
73+
${code}
74+
globalThis.__finalResult = result;
75+
})();
76+
`;
77+
78+
// Execute the original code
79+
const executeScript = await isolate.compileScript(executionCode).catch((err) => {
8280
console.error(err);
8381
return { error: 'Compile Error - ' + err.message };
8482
});
85-
if (script?.error) {
86-
throw new Error(script.error);
83+
if ('error' in executeScript) {
84+
throw new Error(executeScript.error);
8785
}
8886

89-
const rawResult = await script.run(context).catch((err) => {
87+
await executeScript.run(context).catch((err) => {
9088
console.error(err);
91-
return { error: 'Run Error - ' + err.message };
89+
throw new Error('Run Error - ' + err.message);
9290
});
91+
92+
// Try to get the result from the global variable first, then fallback to 'result'
93+
let rawResult = await context.eval('globalThis.__finalResult').catch((err) => {
94+
console.error('Failed to get __finalResult:', err);
95+
return null;
96+
});
97+
9398
if (rawResult?.error) {
9499
throw new Error(rawResult.error);
95100
}
96-
97-
// Transfer the result out of the isolate and parse it
98-
//const serializedResult = rawResult.copySync();
99-
const Output = JSON.parse(rawResult);
100-
101-
return Output;
101+
return { Output: rawResult };
102102
} catch (error) {
103103
console.error(error);
104104
throw new Error(error.message);
105105
}
106106
}
107+
108+
function getParametersString(parameters: string[], inputs: Record<string, any>) {
109+
let params = [];
110+
for (const parameter of parameters) {
111+
if (typeof inputs[parameter] === 'string') {
112+
params.push(`'${inputs[parameter]}'`);
113+
} else {
114+
params.push(`${inputs[parameter]};`);
115+
}
116+
}
117+
return params.join(',');
118+
}
119+
120+
export function generateExecutableCode(code: string, parameters: string[], inputs: Record<string, any>) {
121+
const executableCode = `
122+
${code}
123+
const result = await main(${getParametersString(parameters, inputs)});
124+
`
125+
return executableCode;
126+
}

packages/core/src/subsystems/ComputeManager/Code.service/connectors/ECMASandbox.class.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import { CodeConfig, CodePreparationResult, CodeConnector, CodeInput, CodeDeploy
44
import { AccessRequest } from '@sre/Security/AccessControl/AccessRequest.class';
55
import { Logger } from '@sre/helpers/Log.helper';
66
import axios from 'axios';
7-
import { runJs } from '@sre/helpers/ECMASandbox.helper';
7+
import { generateExecutableCode, runJs } from '@sre/helpers/ECMASandbox.helper';
8+
import { validateAsyncMainFunction } from '@sre/helpers/AWSLambdaCode.helper';
89

910
const console = Logger('ECMASandbox');
1011
export class ECMASandbox extends CodeConnector {
@@ -34,10 +35,20 @@ export class ECMASandbox extends CodeConnector {
3435

3536
public async execute(acRequest: AccessRequest, codeUID: string, inputs: Record<string, any>, config: CodeConfig): Promise<CodeExecutionResult> {
3637
try {
38+
const { isValid, error, parameters } = validateAsyncMainFunction(inputs.code);
39+
if (!isValid) {
40+
return {
41+
output: undefined,
42+
executionTime: 0,
43+
success: false,
44+
errors: [error],
45+
}
46+
}
47+
const executableCode = generateExecutableCode(inputs.code, parameters, inputs.inputs);
3748
if (!this.sandboxUrl) {
3849
// run js code in isolated vm
3950
console.debug('Running code in isolated vm');
40-
const result = await runJs(inputs.code);
51+
const result = await runJs(executableCode);
4152
console.debug(`Code result: ${result}`);
4253
return {
4354
output: result?.Output,
@@ -47,7 +58,7 @@ export class ECMASandbox extends CodeConnector {
4758
};
4859
} else {
4960
console.debug('Running code in remote sandbox');
50-
const result: any = await axios.post(this.sandboxUrl, { code: inputs.code }).catch((error) => ({ error }));
61+
const result: any = await axios.post(this.sandboxUrl, { code: executableCode }).catch((error) => ({ error }));
5162
if (result.error) {
5263

5364
const error = result.error?.response?.data || result.error?.message || result.error.toString() || 'Unknown error';

packages/core/tests/unit/core/ecma-sandbox.test.ts

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ setupSRE({
77
Code: {
88
Connector: 'ECMASandbox',
99
Settings: {
10-
sandboxUrl: 'http://localhost:6100/run-js',
10+
sandboxUrl: 'http://localhost:6100/run-js/v2',
1111
},
1212
},
1313
Log: {
@@ -23,16 +23,36 @@ describe('ECMASandbox Tests', () => {
2323
id: 'test-user',
2424
role: TAccessRole.User,
2525
};
26-
26+
2727
const codeConnector = ConnectorService.getCodeConnector('ECMASandbox');
2828
const result = await codeConnector.agent(mockCandidate.id).execute(Date.now().toString(), {
29-
code: 'let _output=undefined;\nconsole.log("Hello, world!");\n_output=1;',
30-
}, {});
29+
code: `async function main(prompt) { return prompt + ' ' + 'Hello World'; }`,
30+
inputs: {
31+
prompt: 'Say'
32+
}
33+
});
3134

3235
const output = result.output;
33-
34-
expect(output).toBe(1);
36+
expect(output).toBe('Say Hello World');
3537
},
3638
);
39+
it(
40+
'Try to run a simple code without main function',
41+
async () => {
42+
const mockCandidate: IAccessCandidate = {
43+
id: 'test-user',
44+
role: TAccessRole.User,
45+
};
3746

47+
const codeConnector = ConnectorService.getCodeConnector('ECMASandbox');
48+
const result = await codeConnector.agent(mockCandidate.id).execute(Date.now().toString(), {
49+
code: `async function testFunction(prompt) { return prompt + ' ' + 'Hello World'; }`,
50+
inputs: {
51+
prompt: 'Say'
52+
}
53+
});
54+
const error = result.errors;
55+
expect(error).toContain('No main function found at root level');
56+
},
57+
);
3858
});

0 commit comments

Comments
 (0)