@@ -138,13 +138,39 @@ def _is_user_scoped(session_id: Optional[str], filename: str) -> bool:
138138 return session_id is None or _file_has_user_namespace (filename )
139139
140140
141+ def _validate_path_segment (value : str , field_name : str ) -> None :
142+ """Rejects values that could alter the constructed filesystem path.
143+
144+ Args:
145+ value: The caller-supplied identifier (e.g. user_id or session_id).
146+ field_name: Human-readable name used in the error message.
147+
148+ Raises:
149+ InputValidationError: If the value contains path separators, traversal
150+ segments, or null bytes.
151+ """
152+ if not value :
153+ raise InputValidationError (f"{ field_name } must not be empty." )
154+ if "\x00 " in value :
155+ raise InputValidationError (f"{ field_name } must not contain null bytes." )
156+ if "/" in value or "\\ " in value :
157+ raise InputValidationError (
158+ f"{ field_name } { value !r} must not contain path separators."
159+ )
160+ if value in ("." , ".." ) or ".." in value .split ("/" ):
161+ raise InputValidationError (
162+ f"{ field_name } { value !r} must not contain traversal segments."
163+ )
164+
165+
141166def _user_artifacts_dir (base_root : Path ) -> Path :
142167 """Returns the path that stores user-scoped artifacts."""
143168 return base_root / "artifacts"
144169
145170
146171def _session_artifacts_dir (base_root : Path , session_id : str ) -> Path :
147172 """Returns the path that stores session-scoped artifacts."""
173+ _validate_path_segment (session_id , "session_id" )
148174 return base_root / "sessions" / session_id / "artifacts"
149175
150176
@@ -220,6 +246,7 @@ def __init__(self, root_dir: Path | str):
220246
221247 def _base_root (self , user_id : str , / ) -> Path :
222248 """Returns the artifacts root directory for a user."""
249+ _validate_path_segment (user_id , "user_id" )
223250 return self .root_dir / "users" / user_id
224251
225252 def _scope_root (
0 commit comments