|
4 | 4 | */ |
5 | 5 |
|
6 | 6 | import { AgentFlow, Task, TaskConfig } from './index'; |
| 7 | +import * as path from 'path'; |
7 | 8 |
|
8 | 9 | export interface YAMLWorkflowDefinition { |
9 | 10 | name: string; |
@@ -39,8 +40,9 @@ export interface ParsedWorkflow { |
39 | 40 | * Parse YAML string into workflow definition |
40 | 41 | */ |
41 | 42 | export function parseYAMLWorkflow(yamlContent: string): YAMLWorkflowDefinition { |
42 | | - // Simple YAML parser for workflow definitions |
43 | | - // For production, use js-yaml package |
| 43 | + // SECURITY: If migrating to js-yaml, you MUST use: |
| 44 | + // yaml.load(content, { schema: yaml.JSON_SCHEMA }) |
| 45 | + // Never use yaml.load() with DEFAULT_SCHEMA — it enables arbitrary JS execution. |
44 | 46 | const lines = yamlContent.split('\n'); |
45 | 47 | const result: YAMLWorkflowDefinition = { |
46 | 48 | name: '', |
@@ -82,7 +84,15 @@ export function parseYAMLWorkflow(yamlContent: string): YAMLWorkflowDefinition { |
82 | 84 | type: 'agent' |
83 | 85 | }; |
84 | 86 | } else if (currentStep) { |
85 | | - // Step properties |
| 87 | + // Step properties — whitelist allowed keys to prevent injection |
| 88 | + const ALLOWED_STEP_KEYS = new Set([ |
| 89 | + 'type', 'agent', 'tool', 'input', 'output', 'condition', |
| 90 | + 'onError', 'maxRetries', 'timeout', 'loopCondition', 'maxIterations', |
| 91 | + ]); |
| 92 | + if (!ALLOWED_STEP_KEYS.has(key)) { |
| 93 | + // Ignore unknown keys — do not allow arbitrary property injection |
| 94 | + continue; |
| 95 | + } |
86 | 96 | if (key === 'type') currentStep.type = value as any; |
87 | 97 | else if (key === 'agent') currentStep.agent = value; |
88 | 98 | else if (key === 'tool') currentStep.tool = value; |
@@ -265,9 +275,33 @@ function parseValue(value: string): any { |
265 | 275 | export async function loadWorkflowFromFile( |
266 | 276 | filePath: string, |
267 | 277 | agents: Record<string, any> = {}, |
268 | | - tools: Record<string, any> = {} |
| 278 | + tools: Record<string, any> = {}, |
| 279 | + options: { basePath?: string; maxFileSizeBytes?: number } = {} |
269 | 280 | ): Promise<ParsedWorkflow> { |
270 | 281 | const fs = await import('fs/promises'); |
| 282 | + |
| 283 | + // SECURITY: Prevent path traversal |
| 284 | + const normalizedPath = path.normalize(filePath); |
| 285 | + if (normalizedPath.includes('..')) { |
| 286 | + throw new Error('Path traversal detected: ".." is not allowed in file paths'); |
| 287 | + } |
| 288 | + |
| 289 | + // If basePath is specified, ensure resolvedPath stays within it |
| 290 | + if (options.basePath) { |
| 291 | + const resolvedBase = path.resolve(options.basePath); |
| 292 | + const resolvedFile = path.resolve(options.basePath, normalizedPath); |
| 293 | + if (!resolvedFile.startsWith(resolvedBase + path.sep) && resolvedFile !== resolvedBase) { |
| 294 | + throw new Error(`File path must be within base directory: ${options.basePath}`); |
| 295 | + } |
| 296 | + } |
| 297 | + |
| 298 | + // SECURITY: Enforce file size limit (default 1 MB) |
| 299 | + const maxSize = options.maxFileSizeBytes ?? 1_048_576; |
| 300 | + const stat = await fs.stat(filePath); |
| 301 | + if (stat.size > maxSize) { |
| 302 | + throw new Error(`File too large: ${stat.size} bytes exceeds limit of ${maxSize} bytes`); |
| 303 | + } |
| 304 | + |
271 | 305 | const content = await fs.readFile(filePath, 'utf-8'); |
272 | 306 | const definition = parseYAMLWorkflow(content); |
273 | 307 | return createWorkflowFromYAML(definition, agents, tools); |
|
0 commit comments