Skip to content

Commit 384bb06

Browse files
Merge pull request #1285 from MervinPraison/claude/pr-1284-20260407-2042
PR #1284: Changes from Claude
2 parents 2dd1a80 + 09e85a2 commit 384bb06

File tree

3 files changed

+36
-20
lines changed

3 files changed

+36
-20
lines changed

src/praisonai-agents/praisonaiagents/context/monitor.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -542,7 +542,9 @@ def _get_output_path(self, agent_name: str = "") -> Path:
542542
# Verify the resolved path stays within the expected parent dir
543543
resolved = result.resolve()
544544
parent_resolved = self.path.parent.resolve()
545-
if not str(resolved).startswith(str(parent_resolved) + '/') and resolved.parent != parent_resolved:
545+
try:
546+
resolved.relative_to(parent_resolved)
547+
except ValueError:
546548
# Fallback to a safe hash-based name
547549
import hashlib
548550
hash_name = hashlib.sha256(agent_name.encode()).hexdigest()[:16]

src/praisonai-ts/src/workflows/yaml-parser.ts

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,12 @@ export interface ParsedWorkflow {
3636
errors: string[];
3737
}
3838

39+
// Whitelist of allowed step keys to prevent injection
40+
const ALLOWED_STEP_KEYS = new Set([
41+
'type', 'agent', 'tool', 'input', 'output', 'condition',
42+
'onError', 'maxRetries', 'timeout', 'loopCondition', 'maxIterations',
43+
]);
44+
3945
/**
4046
* Parse YAML string into workflow definition
4147
*/
@@ -85,10 +91,6 @@ export function parseYAMLWorkflow(yamlContent: string): YAMLWorkflowDefinition {
8591
};
8692
} else if (currentStep) {
8793
// 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-
]);
9294
if (!ALLOWED_STEP_KEYS.has(key)) {
9395
// Ignore unknown keys — do not allow arbitrary property injection
9496
continue;
@@ -282,27 +284,33 @@ export async function loadWorkflowFromFile(
282284

283285
// SECURITY: Prevent path traversal
284286
const normalizedPath = path.normalize(filePath);
285-
if (normalizedPath.includes('..')) {
286-
throw new Error('Path traversal detected: ".." is not allowed in file paths');
287+
// Check for '..' as path segments (not just substring)
288+
const pathSegments = normalizedPath.split(path.sep);
289+
if (pathSegments.includes('..')) {
290+
throw new Error('Path traversal detected: ".." path segments are not allowed');
287291
}
288292

293+
let effectivePath: string;
289294
// If basePath is specified, ensure resolvedPath stays within it
290295
if (options.basePath) {
291296
const resolvedBase = path.resolve(options.basePath);
292297
const resolvedFile = path.resolve(options.basePath, normalizedPath);
293298
if (!resolvedFile.startsWith(resolvedBase + path.sep) && resolvedFile !== resolvedBase) {
294299
throw new Error(`File path must be within base directory: ${options.basePath}`);
295300
}
301+
effectivePath = resolvedFile;
302+
} else {
303+
effectivePath = path.resolve(normalizedPath);
296304
}
297305

298306
// SECURITY: Enforce file size limit (default 1 MB)
299307
const maxSize = options.maxFileSizeBytes ?? 1_048_576;
300-
const stat = await fs.stat(filePath);
308+
const stat = await fs.stat(effectivePath);
301309
if (stat.size > maxSize) {
302310
throw new Error(`File too large: ${stat.size} bytes exceeds limit of ${maxSize} bytes`);
303311
}
304312

305-
const content = await fs.readFile(filePath, 'utf-8');
313+
const content = await fs.readFile(effectivePath, 'utf-8');
306314
const definition = parseYAMLWorkflow(content);
307315
return createWorkflowFromYAML(definition, agents, tools);
308316
}

src/praisonai/praisonai/cli/features/agent_tools.py

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,20 @@
88
import asyncio
99
import json
1010
import logging
11-
from typing import Callable, Dict, List, TYPE_CHECKING
11+
from typing import Callable, Dict, List, Optional, TYPE_CHECKING
1212

1313
if TYPE_CHECKING:
1414
from .interactive_runtime import InteractiveRuntime
1515
from .code_intelligence import CodeIntelligenceRouter
1616
from .action_orchestrator import ActionOrchestrator
1717

1818
import os
19-
import re
19+
from pathlib import Path
2020

2121
logger = logging.getLogger(__name__)
2222

2323

24-
def _sanitize_filepath(filepath: str, workspace: str = None) -> str:
24+
def _sanitize_filepath(filepath: str, workspace: Optional[str] = None) -> str:
2525
"""Validate and sanitize a filepath against injection attacks.
2626
2727
Raises ValueError if the path is unsafe.
@@ -45,9 +45,11 @@ def _sanitize_filepath(filepath: str, workspace: str = None) -> str:
4545

4646
# If we have a workspace, ensure the resolved path stays within it
4747
if workspace:
48-
resolved = os.path.realpath(os.path.join(workspace, normalized))
49-
ws_resolved = os.path.realpath(workspace)
50-
if not resolved.startswith(ws_resolved + os.sep) and resolved != ws_resolved:
48+
resolved = Path(os.path.realpath(os.path.join(workspace, normalized)))
49+
ws_resolved = Path(os.path.realpath(workspace))
50+
try:
51+
resolved.relative_to(ws_resolved)
52+
except ValueError:
5153
raise ValueError(
5254
f"Path {filepath!r} resolves outside workspace {workspace!r}"
5355
)
@@ -70,9 +72,9 @@ def _sanitize_command(command: str) -> str:
7072
DANGEROUS_PATTERNS = [
7173
'$(', '`', # Command substitution
7274
'&&', '||', # Command chaining
73-
';', # Command separator
74-
'>>', '> ', # Output redirection
75-
'| ', # Pipe
75+
'>>', '>', # Output redirection
76+
'|', ';', '&', # Pipe and separators
77+
'\n', '\r' # Line breaks
7678
]
7779
for pattern in DANGEROUS_PATTERNS:
7880
if pattern in command:
@@ -513,7 +515,9 @@ def read_file(filepath: str) -> str:
513515
# SECURITY: Ensure resolved path stays within workspace
514516
resolved = path.resolve()
515517
ws_resolved = Path(runtime.config.workspace).resolve()
516-
if not str(resolved).startswith(str(ws_resolved) + os.sep) and resolved != ws_resolved:
518+
try:
519+
resolved.relative_to(ws_resolved)
520+
except ValueError:
517521
return json.dumps({"error": f"Path escapes workspace: {filepath}"})
518522

519523
if not resolved.exists():
@@ -556,7 +560,9 @@ def list_files(directory: str = ".", pattern: str = "*") -> str:
556560
# SECURITY: Ensure resolved path stays within workspace
557561
resolved = path.resolve()
558562
ws_resolved = Path(runtime.config.workspace).resolve()
559-
if not str(resolved).startswith(str(ws_resolved) + os.sep) and resolved != ws_resolved:
563+
try:
564+
resolved.relative_to(ws_resolved)
565+
except ValueError:
560566
return json.dumps({"error": f"Directory escapes workspace: {directory}"})
561567

562568
if not resolved.exists():

0 commit comments

Comments
 (0)