Skip to content

Commit c4a6d11

Browse files
authored
fix(condition): used isolated vms for condition block RCE (#2432)
* fix(condition): used isolated vms for condition block RCE * ack PR comment * one more * remove inputForm from sched, update loop condition to also use isolated vm * hide servicenow
1 parent 7b5405e commit c4a6d11

File tree

7 files changed

+154
-64
lines changed

7 files changed

+154
-64
lines changed

apps/sim/blocks/blocks/schedule.ts

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -155,15 +155,6 @@ export const ScheduleBlock: BlockConfig = {
155155
condition: { field: 'scheduleType', value: ['minutes', 'hourly'], not: true },
156156
},
157157

158-
{
159-
id: 'inputFormat',
160-
title: 'Input Format',
161-
type: 'input-format',
162-
description:
163-
'Define input parameters that will be available when the schedule triggers. Use Value to set default values for scheduled executions.',
164-
mode: 'trigger',
165-
},
166-
167158
{
168159
id: 'scheduleSave',
169160
type: 'schedule-save',

apps/sim/blocks/blocks/servicenow.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export const ServiceNowBlock: BlockConfig<ServiceNowResponse> = {
88
name: 'ServiceNow',
99
description: 'Create, read, update, delete, and bulk import ServiceNow records',
1010
authMode: AuthMode.OAuth,
11+
hideFromToolbar: true,
1112
longDescription:
1213
'Integrate ServiceNow into your workflow. Can create, read, update, and delete records in any ServiceNow table (incidents, tasks, users, etc.). Supports bulk import operations for data migration and ETL.',
1314
docsLink: 'https://docs.sim.ai/tools/servicenow',

apps/sim/executor/handlers/condition/condition-handler.test.ts

Lines changed: 45 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,47 @@
1-
import '@/executor/__test-utils__/mock-dependencies'
2-
31
import { beforeEach, describe, expect, it, vi } from 'vitest'
42
import { BlockType } from '@/executor/constants'
53
import { ConditionBlockHandler } from '@/executor/handlers/condition/condition-handler'
64
import type { BlockState, ExecutionContext } from '@/executor/types'
75
import type { SerializedBlock, SerializedWorkflow } from '@/serializer/types'
86

7+
vi.mock('@/lib/logs/console/logger', () => ({
8+
createLogger: vi.fn(() => ({
9+
info: vi.fn(),
10+
error: vi.fn(),
11+
warn: vi.fn(),
12+
debug: vi.fn(),
13+
})),
14+
}))
15+
16+
vi.mock('@/lib/core/utils/request', () => ({
17+
generateRequestId: vi.fn(() => 'test-request-id'),
18+
}))
19+
20+
vi.mock('@/lib/execution/isolated-vm', () => ({
21+
executeInIsolatedVM: vi.fn(),
22+
}))
23+
24+
import { executeInIsolatedVM } from '@/lib/execution/isolated-vm'
25+
26+
const mockExecuteInIsolatedVM = executeInIsolatedVM as ReturnType<typeof vi.fn>
27+
28+
function simulateIsolatedVMExecution(
29+
code: string,
30+
contextVariables: Record<string, unknown>
31+
): { result: unknown; stdout: string; error?: { message: string; name: string } } {
32+
try {
33+
const fn = new Function(...Object.keys(contextVariables), code)
34+
const result = fn(...Object.values(contextVariables))
35+
return { result, stdout: '' }
36+
} catch (error: any) {
37+
return {
38+
result: null,
39+
stdout: '',
40+
error: { message: error.message, name: error.name || 'Error' },
41+
}
42+
}
43+
}
44+
945
describe('ConditionBlockHandler', () => {
1046
let handler: ConditionBlockHandler
1147
let mockBlock: SerializedBlock
@@ -18,7 +54,6 @@ describe('ConditionBlockHandler', () => {
1854
let mockPathTracker: any
1955

2056
beforeEach(() => {
21-
// Define blocks first
2257
mockSourceBlock = {
2358
id: 'source-block-1',
2459
metadata: { id: 'source', name: 'Source Block' },
@@ -33,7 +68,7 @@ describe('ConditionBlockHandler', () => {
3368
metadata: { id: BlockType.CONDITION, name: 'Test Condition' },
3469
position: { x: 50, y: 50 },
3570
config: { tool: BlockType.CONDITION, params: {} },
36-
inputs: { conditions: 'json' }, // Corrected based on previous step
71+
inputs: { conditions: 'json' },
3772
outputs: {},
3873
enabled: true,
3974
}
@@ -56,7 +91,6 @@ describe('ConditionBlockHandler', () => {
5691
enabled: true,
5792
}
5893

59-
// Then define workflow using the block objects
6094
mockWorkflow = {
6195
blocks: [mockSourceBlock, mockBlock, mockTargetBlock1, mockTargetBlock2],
6296
connections: [
@@ -84,7 +118,6 @@ describe('ConditionBlockHandler', () => {
84118

85119
handler = new ConditionBlockHandler(mockPathTracker, mockResolver)
86120

87-
// Define mock context *after* workflow and blocks are set up
88121
mockContext = {
89122
workflowId: 'test-workflow-id',
90123
blockStates: new Map<string, BlockState>([
@@ -99,7 +132,7 @@ describe('ConditionBlockHandler', () => {
99132
]),
100133
blockLogs: [],
101134
metadata: { duration: 0 },
102-
environmentVariables: {}, // Now set the context's env vars
135+
environmentVariables: {},
103136
decisions: { router: new Map(), condition: new Map() },
104137
loopExecutions: new Map(),
105138
executedBlocks: new Set([mockSourceBlock.id]),
@@ -108,11 +141,11 @@ describe('ConditionBlockHandler', () => {
108141
completedLoops: new Set(),
109142
}
110143

111-
// Reset mocks using vi
112144
vi.clearAllMocks()
113145

114-
// Default mock implementations - Removed as it's in the shared mock now
115-
// mockResolver.resolveBlockReferences.mockImplementation((value) => value)
146+
mockExecuteInIsolatedVM.mockImplementation(async ({ code, contextVariables }) => {
147+
return simulateIsolatedVMExecution(code, contextVariables)
148+
})
116149
})
117150

118151
it('should handle condition blocks', () => {
@@ -141,7 +174,6 @@ describe('ConditionBlockHandler', () => {
141174
selectedOption: 'cond1',
142175
}
143176

144-
// Mock the full resolution pipeline
145177
mockResolver.resolveVariableReferences.mockReturnValue('context.value > 5')
146178
mockResolver.resolveBlockReferences.mockReturnValue('context.value > 5')
147179
mockResolver.resolveEnvVariables.mockReturnValue('context.value > 5')
@@ -182,7 +214,6 @@ describe('ConditionBlockHandler', () => {
182214
selectedOption: 'else1',
183215
}
184216

185-
// Mock the full resolution pipeline
186217
mockResolver.resolveVariableReferences.mockReturnValue('context.value < 0')
187218
mockResolver.resolveBlockReferences.mockReturnValue('context.value < 0')
188219
mockResolver.resolveEnvVariables.mockReturnValue('context.value < 0')
@@ -207,7 +238,7 @@ describe('ConditionBlockHandler', () => {
207238
const inputs = { conditions: '{ "invalid json ' }
208239

209240
await expect(handler.execute(mockContext, mockBlock, inputs)).rejects.toThrow(
210-
/^Invalid conditions format: Unterminated string.*/
241+
/^Invalid conditions format:/
211242
)
212243
})
213244

@@ -218,7 +249,6 @@ describe('ConditionBlockHandler', () => {
218249
]
219250
const inputs = { conditions: JSON.stringify(conditions) }
220251

221-
// Mock the full resolution pipeline
222252
mockResolver.resolveVariableReferences.mockReturnValue('{{source-block-1.value}} > 5')
223253
mockResolver.resolveBlockReferences.mockReturnValue('10 > 5')
224254
mockResolver.resolveEnvVariables.mockReturnValue('10 > 5')
@@ -245,7 +275,6 @@ describe('ConditionBlockHandler', () => {
245275
]
246276
const inputs = { conditions: JSON.stringify(conditions) }
247277

248-
// Mock the full resolution pipeline for variable resolution
249278
mockResolver.resolveVariableReferences.mockReturnValue('"john" !== null')
250279
mockResolver.resolveBlockReferences.mockReturnValue('"john" !== null')
251280
mockResolver.resolveEnvVariables.mockReturnValue('"john" !== null')
@@ -272,7 +301,6 @@ describe('ConditionBlockHandler', () => {
272301
]
273302
const inputs = { conditions: JSON.stringify(conditions) }
274303

275-
// Mock the full resolution pipeline for env variable resolution
276304
mockResolver.resolveVariableReferences.mockReturnValue('{{POOP}} === "hi"')
277305
mockResolver.resolveBlockReferences.mockReturnValue('{{POOP}} === "hi"')
278306
mockResolver.resolveEnvVariables.mockReturnValue('"hi" === "hi"')
@@ -300,7 +328,6 @@ describe('ConditionBlockHandler', () => {
300328
const inputs = { conditions: JSON.stringify(conditions) }
301329

302330
const resolutionError = new Error('Could not resolve reference: invalid-ref')
303-
// Mock the pipeline to throw at the variable resolution stage
304331
mockResolver.resolveVariableReferences.mockImplementation(() => {
305332
throw resolutionError
306333
})
@@ -317,23 +344,21 @@ describe('ConditionBlockHandler', () => {
317344
]
318345
const inputs = { conditions: JSON.stringify(conditions) }
319346

320-
// Mock the full resolution pipeline
321347
mockResolver.resolveVariableReferences.mockReturnValue(
322348
'context.nonExistentProperty.doSomething()'
323349
)
324350
mockResolver.resolveBlockReferences.mockReturnValue('context.nonExistentProperty.doSomething()')
325351
mockResolver.resolveEnvVariables.mockReturnValue('context.nonExistentProperty.doSomething()')
326352

327353
await expect(handler.execute(mockContext, mockBlock, inputs)).rejects.toThrow(
328-
/^Evaluation error in condition "if": Evaluation error in condition: Cannot read properties of undefined \(reading 'doSomething'\)\. \(Resolved: context\.nonExistentProperty\.doSomething\(\)\)$/
354+
/Evaluation error in condition "if".*doSomething/
329355
)
330356
})
331357

332358
it('should handle missing source block output gracefully', async () => {
333359
const conditions = [{ id: 'cond1', title: 'if', value: 'true' }]
334360
const inputs = { conditions: JSON.stringify(conditions) }
335361

336-
// Create a new context with empty blockStates instead of trying to delete from readonly map
337362
const contextWithoutSource = {
338363
...mockContext,
339364
blockStates: new Map<string, BlockState>(),
@@ -355,7 +380,6 @@ describe('ConditionBlockHandler', () => {
355380

356381
mockContext.workflow!.blocks = [mockSourceBlock, mockBlock, mockTargetBlock2]
357382

358-
// Mock the full resolution pipeline
359383
mockResolver.resolveVariableReferences.mockReturnValue('true')
360384
mockResolver.resolveBlockReferences.mockReturnValue('true')
361385
mockResolver.resolveEnvVariables.mockReturnValue('true')
@@ -381,7 +405,6 @@ describe('ConditionBlockHandler', () => {
381405
},
382406
]
383407

384-
// Mock the full resolution pipeline
385408
mockResolver.resolveVariableReferences
386409
.mockReturnValueOnce('false')
387410
.mockReturnValueOnce('context.value === 99')
@@ -394,12 +417,10 @@ describe('ConditionBlockHandler', () => {
394417

395418
const result = await handler.execute(mockContext, mockBlock, inputs)
396419

397-
// Should return success with no path selected (branch ends gracefully)
398420
expect((result as any).conditionResult).toBe(false)
399421
expect((result as any).selectedPath).toBeNull()
400422
expect((result as any).selectedConditionId).toBeNull()
401423
expect((result as any).selectedOption).toBeNull()
402-
// Decision should not be set when no condition matches
403424
expect(mockContext.decisions.condition.has(mockBlock.id)).toBe(false)
404425
})
405426

@@ -410,7 +431,6 @@ describe('ConditionBlockHandler', () => {
410431
]
411432
const inputs = { conditions: JSON.stringify(conditions) }
412433

413-
// Mock the full resolution pipeline
414434
mockResolver.resolveVariableReferences.mockReturnValue('context.item === "apple"')
415435
mockResolver.resolveBlockReferences.mockReturnValue('context.item === "apple"')
416436
mockResolver.resolveEnvVariables.mockReturnValue('context.item === "apple"')

apps/sim/executor/handlers/condition/condition-handler.ts

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { generateRequestId } from '@/lib/core/utils/request'
2+
import { executeInIsolatedVM } from '@/lib/execution/isolated-vm'
13
import { createLogger } from '@/lib/logs/console/logger'
24
import type { BlockOutput } from '@/blocks/types'
35
import { BlockType, CONDITION, DEFAULTS, EDGE } from '@/executor/constants'
@@ -6,6 +8,8 @@ import type { SerializedBlock } from '@/serializer/types'
68

79
const logger = createLogger('ConditionBlockHandler')
810

11+
const CONDITION_TIMEOUT_MS = 5000
12+
913
/**
1014
* Evaluates a single condition expression with variable/block reference resolution
1115
* Returns true if condition is met, false otherwise
@@ -35,11 +39,32 @@ export async function evaluateConditionExpression(
3539
}
3640

3741
try {
38-
const conditionMet = new Function(
39-
'context',
40-
`with(context) { return ${resolvedConditionValue} }`
41-
)(evalContext)
42-
return Boolean(conditionMet)
42+
const requestId = generateRequestId()
43+
44+
const code = `return Boolean(${resolvedConditionValue})`
45+
46+
const result = await executeInIsolatedVM({
47+
code,
48+
params: {},
49+
envVars: {},
50+
contextVariables: { context: evalContext },
51+
timeoutMs: CONDITION_TIMEOUT_MS,
52+
requestId,
53+
})
54+
55+
if (result.error) {
56+
logger.error(`Failed to evaluate condition: ${result.error.message}`, {
57+
originalCondition: conditionExpression,
58+
resolvedCondition: resolvedConditionValue,
59+
evalContext,
60+
error: result.error,
61+
})
62+
throw new Error(
63+
`Evaluation error in condition: ${result.error.message}. (Resolved: ${resolvedConditionValue})`
64+
)
65+
}
66+
67+
return Boolean(result.result)
4368
} catch (evalError: any) {
4469
logger.error(`Failed to evaluate condition: ${evalError.message}`, {
4570
originalCondition: conditionExpression,
@@ -87,7 +112,6 @@ export class ConditionBlockHandler implements BlockHandler {
87112
block
88113
)
89114

90-
// Handle case where no condition matched and no else exists - branch ends gracefully
91115
if (!selectedConnection || !selectedCondition) {
92116
return {
93117
...((sourceOutput as any) || {}),
@@ -206,14 +230,12 @@ export class ConditionBlockHandler implements BlockHandler {
206230
if (elseConnection) {
207231
return { selectedConnection: elseConnection, selectedCondition: elseCondition }
208232
}
209-
// Else exists but has no connection - treat as no match, branch ends
210233
logger.info(`No condition matched and else has no connection - branch ending`, {
211234
blockId: block.id,
212235
})
213236
return { selectedConnection: null, selectedCondition: null }
214237
}
215238

216-
// No condition matched and no else exists - branch ends gracefully
217239
logger.info(`No condition matched and no else block - branch ending`, { blockId: block.id })
218240
return { selectedConnection: null, selectedCondition: null }
219241
}

0 commit comments

Comments
 (0)