@@ -401,7 +401,7 @@ def _load_skill_from_gcs_dir(
401401 f" name '{ skill_name_expected } '."
402402 )
403403
404- def _load_files_in_dir (subdir : str ) -> Dict [str , Union [ str , bytes ] ]:
404+ def _load_files_in_dir (subdir : str ) -> dict [str , str | bytes ]:
405405 prefix = f"{ skill_dir_prefix } { subdir } /"
406406 blobs = bucket .list_blobs (prefix = prefix )
407407 result = {}
@@ -411,17 +411,30 @@ def _load_files_in_dir(subdir: str) -> Dict[str, Union[str, bytes]]:
411411 if not relative_path :
412412 continue
413413
414- # Prevent path traversal via malicious GCS blob names
415- normalized = os .path .normpath (relative_path )
416- if normalized .startswith ('..' ) or os .path .isabs (normalized ):
414+ # Use PurePosixPath for platform-independent GCS path validation
415+ p = pathlib .PurePosixPath (relative_path )
416+
417+ # Reject absolute paths and traversal sequences
418+ if p .is_absolute () or ".." in p .parts :
417419 raise ValueError (
418420 f"Unsafe path in skill resource: { relative_path !r} "
419421 )
420422
423+ normalized = p .as_posix ()
424+
425+ # Prevent silent file overwrites via path aliasing
426+ if normalized in result :
427+ raise ValueError (
428+ f"Duplicate normalized path detected: { normalized !r} "
429+ )
430+
431+ # NOTE: Final path safety enforced during materialization
432+ # via realpath + commonpath checks in skill_toolset.py
421433 try :
422434 result [normalized ] = blob .download_as_text ()
423435 except UnicodeDecodeError :
424436 result [normalized ] = blob .download_as_bytes ()
437+
425438 return result
426439
427440 references = _load_files_in_dir ("references" )
0 commit comments