88import asyncio
99import json
1010import logging
11- from typing import Callable , Dict , List , TYPE_CHECKING
11+ from typing import Callable , Dict , List , Optional , TYPE_CHECKING
1212
1313if TYPE_CHECKING :
1414 from .interactive_runtime import InteractiveRuntime
1515 from .code_intelligence import CodeIntelligenceRouter
1616 from .action_orchestrator import ActionOrchestrator
1717
1818import os
19- import re
19+ from pathlib import Path
2020
2121logger = 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