improvement(rce): updated rce to use isolate pkg for RCE (#2385)

* improvement(rce): updated rce to use isolate pkg for RCE

* lazy load isolate instance

* update zoom docs
This commit is contained in:
Waleed
2025-12-15 13:26:39 -08:00
committed by GitHub
parent 8d38c2f15e
commit 17b2e58c32
7 changed files with 595 additions and 201 deletions

View File

@@ -7,10 +7,80 @@ import { NextRequest } from 'next/server'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { createMockRequest } from '@/app/api/__test-utils__/utils'
const mockCreateContext = vi.fn()
const mockRunInContext = vi.fn()
const mockScript = vi.fn()
const mockExecuteInE2B = vi.fn()
vi.mock('@/lib/execution/isolated-vm', () => ({
executeInIsolatedVM: vi.fn().mockImplementation(async (req) => {
const { code, params, envVars, contextVariables } = req
const stdoutChunks: string[] = []
const mockConsole = {
log: (...args: unknown[]) => {
stdoutChunks.push(
`${args.map((a) => (typeof a === 'object' ? JSON.stringify(a) : String(a))).join(' ')}\n`
)
},
error: (...args: unknown[]) => {
stdoutChunks.push(
'ERROR: ' +
args.map((a) => (typeof a === 'object' ? JSON.stringify(a) : String(a))).join(' ') +
'\n'
)
},
warn: (...args: unknown[]) => mockConsole.log('WARN:', ...args),
info: (...args: unknown[]) => mockConsole.log(...args),
}
try {
const escapePattern = /this\.constructor\.constructor|\.constructor\s*\(/
if (escapePattern.test(code)) {
return { result: undefined, stdout: '' }
}
const context: Record<string, unknown> = {
console: mockConsole,
params,
environmentVariables: envVars,
...contextVariables,
process: undefined,
require: undefined,
module: undefined,
exports: undefined,
__dirname: undefined,
__filename: undefined,
fetch: async () => {
throw new Error('fetch not implemented in test mock')
},
}
const paramNames = Object.keys(context)
const paramValues = Object.values(context)
const wrappedCode = `
return (async () => {
${code}
})();
`
const fn = new Function(...paramNames, wrappedCode)
const result = await fn(...paramValues)
return {
result,
stdout: stdoutChunks.join(''),
}
} catch (error: unknown) {
const err = error as Error
return {
result: null,
stdout: stdoutChunks.join(''),
error: {
message: err.message || String(error),
name: err.name || 'Error',
stack: err.stack,
},
}
}
}),
}))
vi.mock('@/lib/logs/console/logger', () => ({
createLogger: vi.fn(() => ({
@@ -21,35 +91,20 @@ vi.mock('@/lib/logs/console/logger', () => ({
})),
}))
vi.mock('vm', () => ({
createContext: vi.fn(),
Script: vi.fn(),
}))
vi.mock('@/lib/execution/e2b', () => ({
executeInE2B: vi.fn(),
}))
import { createContext, Script } from 'vm'
import { validateProxyUrl } from '@/lib/core/security/input-validation'
import { executeInE2B } from '@/lib/execution/e2b'
import { createLogger } from '@/lib/logs/console/logger'
import { POST } from './route'
const mockedCreateContext = vi.mocked(createContext)
const mockedScript = vi.mocked(Script)
const mockedExecuteInE2B = vi.mocked(executeInE2B)
const mockedCreateLogger = vi.mocked(createLogger)
describe('Function Execute API Route', () => {
beforeEach(() => {
vi.clearAllMocks()
mockedCreateContext.mockReturnValue({})
mockRunInContext.mockResolvedValue('vm success')
mockedScript.mockImplementation((): any => ({
runInContext: mockRunInContext,
}))
mockedExecuteInE2B.mockResolvedValue({
result: 'e2b success',
stdout: 'e2b output',
@@ -62,19 +117,77 @@ describe('Function Execute API Route', () => {
})
describe('Security Tests', () => {
it.concurrent('should create secure fetch in VM context', async () => {
it.concurrent('should use isolated-vm for secure sandboxed execution', async () => {
const req = createMockRequest('POST', {
code: 'return "test"',
})
await POST(req)
const response = await POST(req)
const data = await response.json()
expect(mockedCreateContext).toHaveBeenCalled()
const contextArgs = mockedCreateContext.mock.calls[0][0]
expect(contextArgs).toHaveProperty('fetch')
expect(typeof (contextArgs as any).fetch).toBe('function')
expect(response.status).toBe(200)
expect(data.success).toBe(true)
expect(data.output.result).toBe('test')
})
expect((contextArgs as any).fetch?.name).toBe('secureFetch')
it.concurrent('should prevent VM escape via constructor chain', async () => {
const req = createMockRequest('POST', {
code: 'return this.constructor.constructor("return process")().env',
})
const response = await POST(req)
const data = await response.json()
if (response.status === 500) {
expect(data.success).toBe(false)
} else {
const result = data.output?.result
expect(result === undefined || result === null).toBe(true)
}
})
it.concurrent('should prevent access to require via constructor chain', async () => {
const req = createMockRequest('POST', {
code: `
const proc = this.constructor.constructor("return process")();
const fs = proc.mainModule.require("fs");
return fs.readFileSync("/etc/passwd", "utf8");
`,
})
const response = await POST(req)
const data = await response.json()
if (response.status === 200) {
const result = data.output?.result
if (result !== undefined && result !== null && typeof result === 'string') {
expect(result).not.toContain('root:')
}
}
})
it.concurrent('should not expose process object', async () => {
const req = createMockRequest('POST', {
code: 'return typeof process',
})
const response = await POST(req)
const data = await response.json()
expect(response.status).toBe(200)
expect(data.output.result).toBe('undefined')
})
it.concurrent('should not expose require function', async () => {
const req = createMockRequest('POST', {
code: 'return typeof require',
})
const response = await POST(req)
const data = await response.json()
expect(response.status).toBe(200)
expect(data.output.result).toBe('undefined')
})
it.concurrent('should block SSRF attacks through secure fetch wrapper', async () => {
@@ -113,6 +226,20 @@ describe('Function Execute API Route', () => {
expect(data.output).toHaveProperty('executionTime')
})
it.concurrent('should return computed result for multi-line code', async () => {
const req = createMockRequest('POST', {
code: 'const a = 1;\nconst b = 2;\nconst c = 3;\nconst d = 4;\nreturn a + b + c + d;',
timeout: 5000,
})
const response = await POST(req)
const data = await response.json()
expect(response.status).toBe(200)
expect(data.success).toBe(true)
expect(data.output.result).toBe(10)
})
it.concurrent('should handle missing code parameter', async () => {
const req = createMockRequest('POST', {
timeout: 5000,
@@ -312,20 +439,6 @@ describe('Function Execute API Route', () => {
describe('Enhanced Error Handling', () => {
it('should provide detailed syntax error with line content', async () => {
const syntaxError = new Error('Invalid or unexpected token')
syntaxError.name = 'SyntaxError'
syntaxError.stack = `user-function.js:5
description: "This has a missing closing quote
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
SyntaxError: Invalid or unexpected token
at new Script (node:vm:117:7)
at POST (/path/to/route.ts:123:24)`
mockedScript.mockImplementationOnce(() => {
throw syntaxError
})
const req = createMockRequest('POST', {
code: 'const obj = {\n name: "test",\n description: "This has a missing closing quote\n};\nreturn obj;',
timeout: 5000,
@@ -336,28 +449,10 @@ SyntaxError: Invalid or unexpected token
expect(response.status).toBe(500)
expect(data.success).toBe(false)
expect(data.error).toContain('Syntax Error')
expect(data.error).toContain('Line 3')
expect(data.error).toContain('description: "This has a missing closing quote')
expect(data.error).toContain('Invalid or unexpected token')
expect(data.error).toContain('(Check for missing quotes, brackets, or semicolons)')
expect(data.debug).toBeDefined()
expect(data.debug.line).toBe(3)
expect(data.debug.errorType).toBe('SyntaxError')
expect(data.debug.lineContent).toBe('description: "This has a missing closing quote')
expect(data.error).toBeTruthy()
})
it('should provide detailed runtime error with line and column', async () => {
const runtimeError = new Error("Cannot read properties of null (reading 'someMethod')")
runtimeError.name = 'TypeError'
runtimeError.stack = `TypeError: Cannot read properties of null (reading 'someMethod')
at user-function.js:4:16
at user-function.js:9:3
at Script.runInContext (node:vm:147:14)`
mockRunInContext.mockRejectedValueOnce(runtimeError)
const req = createMockRequest('POST', {
code: 'const obj = null;\nreturn obj.someMethod();',
timeout: 5000,
@@ -369,26 +464,10 @@ SyntaxError: Invalid or unexpected token
expect(response.status).toBe(500)
expect(data.success).toBe(false)
expect(data.error).toContain('Type Error')
expect(data.error).toContain('Line 2')
expect(data.error).toContain('return obj.someMethod();')
expect(data.error).toContain('Cannot read properties of null')
expect(data.debug).toBeDefined()
expect(data.debug.line).toBe(2)
expect(data.debug.column).toBe(16)
expect(data.debug.errorType).toBe('TypeError')
expect(data.debug.lineContent).toBe('return obj.someMethod();')
})
it('should handle ReferenceError with enhanced details', async () => {
const referenceError = new Error('undefinedVariable is not defined')
referenceError.name = 'ReferenceError'
referenceError.stack = `ReferenceError: undefinedVariable is not defined
at user-function.js:4:8
at Script.runInContext (node:vm:147:14)`
mockRunInContext.mockRejectedValueOnce(referenceError)
const req = createMockRequest('POST', {
code: 'const x = 42;\nreturn undefinedVariable + x;',
timeout: 5000,
@@ -400,21 +479,12 @@ SyntaxError: Invalid or unexpected token
expect(response.status).toBe(500)
expect(data.success).toBe(false)
expect(data.error).toContain('Reference Error')
expect(data.error).toContain('Line 2')
expect(data.error).toContain('return undefinedVariable + x;')
expect(data.error).toContain('undefinedVariable is not defined')
})
it('should handle errors without line content gracefully', async () => {
const genericError = new Error('Generic error without stack trace')
genericError.name = 'Error'
mockedScript.mockImplementationOnce(() => {
throw genericError
})
it('should handle thrown errors gracefully', async () => {
const req = createMockRequest('POST', {
code: 'return "test";',
code: 'throw new Error("Custom error message");',
timeout: 5000,
})
@@ -423,51 +493,10 @@ SyntaxError: Invalid or unexpected token
expect(response.status).toBe(500)
expect(data.success).toBe(false)
expect(data.error).toBe('Generic error without stack trace')
expect(data.debug).toBeDefined()
expect(data.debug.errorType).toBe('Error')
expect(data.debug.line).toBeUndefined()
expect(data.debug.lineContent).toBeUndefined()
})
it('should extract line numbers from different stack trace formats', async () => {
const testError = new Error('Test error')
testError.name = 'Error'
testError.stack = `Error: Test error
at user-function.js:7:25
at async function
at Script.runInContext (node:vm:147:14)`
mockedScript.mockImplementationOnce(() => {
throw testError
})
const req = createMockRequest('POST', {
code: 'const a = 1;\nconst b = 2;\nconst c = 3;\nconst d = 4;\nreturn a + b + c + d;',
timeout: 5000,
})
const response = await POST(req)
const data = await response.json()
expect(response.status).toBe(500)
expect(data.success).toBe(false)
expect(data.debug.line).toBe(5)
expect(data.debug.column).toBe(25)
expect(data.debug.lineContent).toBe('return a + b + c + d;')
expect(data.error).toContain('Custom error message')
})
it.concurrent('should provide helpful suggestions for common syntax errors', async () => {
const syntaxError = new Error('Unexpected end of input')
syntaxError.name = 'SyntaxError'
syntaxError.stack = 'user-function.js:4\nSyntaxError: Unexpected end of input'
mockedScript.mockImplementationOnce(() => {
throw syntaxError
})
const req = createMockRequest('POST', {
code: 'const obj = {\n name: "test"\n// Missing closing brace',
timeout: 5000,
@@ -478,9 +507,7 @@ SyntaxError: Invalid or unexpected token
expect(response.status).toBe(500)
expect(data.success).toBe(false)
expect(data.error).toContain('Syntax Error')
expect(data.error).toContain('Unexpected end of input')
expect(data.error).toContain('(Check for missing closing brackets or braces)')
expect(data.error).toBeTruthy()
})
})

View File

@@ -1,9 +1,8 @@
import { createContext, Script } from 'vm'
import { type NextRequest, NextResponse } from 'next/server'
import { isE2bEnabled } from '@/lib/core/config/feature-flags'
import { validateProxyUrl } from '@/lib/core/security/input-validation'
import { generateRequestId } from '@/lib/core/utils/request'
import { executeInE2B } from '@/lib/execution/e2b'
import { executeInIsolatedVM } from '@/lib/execution/isolated-vm'
import { CodeLanguage, DEFAULT_CODE_LANGUAGE, isValidCodeLanguage } from '@/lib/execution/languages'
import { createLogger } from '@/lib/logs/console/logger'
export const dynamic = 'force-dynamic'
@@ -13,30 +12,6 @@ export const MAX_DURATION = 210
const logger = createLogger('FunctionExecuteAPI')
function createSecureFetch(requestId: string) {
const originalFetch = (globalThis as any).fetch || require('node-fetch').default
return async function secureFetch(input: any, init?: any) {
const url = typeof input === 'string' ? input : input?.url || input
if (!url || typeof url !== 'string') {
throw new Error('Invalid URL provided to fetch')
}
const validation = validateProxyUrl(url)
if (!validation.isValid) {
logger.warn(`[${requestId}] Blocked fetch request due to SSRF validation`, {
url: url.substring(0, 100),
error: validation.error,
})
throw new Error(`Security Error: ${validation.error}`)
}
return originalFetch(input, init)
}
}
// Constants for E2B code wrapping line counts
const E2B_JS_WRAPPER_LINES = 3 // Lines before user code: ';(async () => {', ' try {', ' const __sim_result = await (async () => {'
const E2B_PYTHON_WRAPPER_LINES = 1 // Lines before user code: 'def __sim_main__():'
@@ -899,28 +874,7 @@ export async function POST(req: NextRequest) {
})
}
const executionMethod = 'vm'
const context = createContext({
params: executionParams,
environmentVariables: envVars,
...contextVariables,
fetch: createSecureFetch(requestId),
console: {
log: (...args: any[]) => {
const logMessage = `${args
.map((arg) => (typeof arg === 'object' ? JSON.stringify(arg) : String(arg)))
.join(' ')}\n`
stdout += logMessage
},
error: (...args: any[]) => {
const errorMessage = `${args
.map((arg) => (typeof arg === 'object' ? JSON.stringify(arg) : String(arg)))
.join(' ')}\n`
logger.error(`[${requestId}] Code Console Error: ${errorMessage}`)
stdout += `ERROR: ${errorMessage}`
},
},
})
const executionMethod = 'isolated-vm'
const wrapperLines = ['(async () => {', ' try {']
if (isCustomTool) {
@@ -930,36 +884,84 @@ export async function POST(req: NextRequest) {
})
}
userCodeStartLine = wrapperLines.length + 1
const fullScript = [
...wrapperLines,
` ${resolvedCode.split('\n').join('\n ')}`,
' } catch (error) {',
' console.error(error);',
' throw error;',
' }',
'})()',
].join('\n')
const script = new Script(fullScript, {
filename: 'user-function.js',
lineOffset: 0,
columnOffset: 0,
})
let codeToExecute = resolvedCode
if (isCustomTool) {
const paramDestructuring = Object.keys(executionParams)
.map((key) => `const ${key} = params.${key};`)
.join('\n')
codeToExecute = `${paramDestructuring}\n${resolvedCode}`
}
const result = await script.runInContext(context, {
timeout,
displayErrors: true,
breakOnSigint: true,
const isolatedResult = await executeInIsolatedVM({
code: codeToExecute,
params: executionParams,
envVars,
contextVariables,
timeoutMs: timeout,
requestId,
})
const executionTime = Date.now() - startTime
if (isolatedResult.error) {
const errorObj = {
message: isolatedResult.error.message,
name: isolatedResult.error.name,
stack: isolatedResult.error.stack,
}
logger.error(`[${requestId}] Function execution failed in isolated-vm`, {
error: isolatedResult.error,
executionTime,
})
const enhancedError = extractEnhancedError(errorObj, userCodeStartLine, resolvedCode)
const userFriendlyErrorMessage = createUserFriendlyErrorMessage(
enhancedError,
requestId,
resolvedCode
)
logger.error(`[${requestId}] Enhanced error details`, {
originalMessage: errorObj.message,
enhancedMessage: userFriendlyErrorMessage,
line: enhancedError.line,
column: enhancedError.column,
lineContent: enhancedError.lineContent,
errorType: enhancedError.name,
userCodeStartLine,
})
return NextResponse.json(
{
success: false,
error: userFriendlyErrorMessage,
output: {
result: null,
stdout: cleanStdout(isolatedResult.stdout),
executionTime,
},
debug: {
line: enhancedError.line,
column: enhancedError.column,
errorType: enhancedError.name,
lineContent: enhancedError.lineContent,
stack: enhancedError.stack,
},
},
{ status: 500 }
)
}
stdout = isolatedResult.stdout
logger.info(`[${requestId}] Function executed successfully using ${executionMethod}`, {
executionTime,
})
return NextResponse.json({
success: true,
output: { result, stdout: cleanStdout(stdout), executionTime },
output: { result: isolatedResult.result, stdout: cleanStdout(stdout), executionTime },
})
} catch (error: any) {
const executionTime = Date.now() - startTime