@@ -53,14 +53,28 @@ def extract_zip(file_path: str, extract_to: str) -> None:
5353 max_size = 10 * 1024 * 1024 * 1024 # 10GB limit
5454
5555 with zipfile .ZipFile (file_path , "r" ) as zip_ref :
56+ # Get absolute path of extract_to for proper comparison
57+ extract_to_abs = os .path .abspath (extract_to )
58+
5659 # Security: Check for path traversal in zip entries
5760 for member in zip_ref .namelist ():
58- # Normalize the path and check for path traversal attempts
59- member_path = os .path .normpath (os .path .join (extract_to , member ))
60- if not member_path .startswith (os .path .abspath (extract_to )):
61- raise ValueError (f"Attempted path traversal in zip file: { member } " )
62- if ".." in member or member .startswith ("/" ):
61+ # Check for explicit path traversal attempts (../ or ..\ patterns)
62+ # Allow filenames containing ".." as a substring (e.g., "file..name.txt")
63+ if "../" in member or "..\\ " in member or member .startswith ("../" ) or member .startswith ("..\\ " ):
6364 raise ValueError (f"Invalid path in zip file: { member } " )
65+ # Check for absolute paths
66+ if member .startswith ("/" ) or (os .name == "nt" and len (member ) > 1 and member [1 ] == ":" ):
67+ raise ValueError (f"Invalid path in zip file: { member } " )
68+
69+ # Normalize the member path (remove leading slashes and normalize)
70+ member_normalized = os .path .normpath (member .lstrip ("/" ))
71+
72+ # Join with extract_to and normalize to get the full path
73+ member_path = os .path .normpath (os .path .join (extract_to_abs , member_normalized ))
74+
75+ # Check if the resolved path is within extract_to directory
76+ if not member_path .startswith (extract_to_abs + os .sep ) and member_path != extract_to_abs :
77+ raise ValueError (f"Attempted path traversal in zip file: { member } " )
6478
6579 # Security: Check for zip bombs
6680 total_uncompressed_size = sum (info .file_size for info in zip_ref .infolist ())
@@ -83,14 +97,28 @@ def extract_gz(file_path: str, extract_to: str) -> None:
8397def extract_tar (file_path : str , extract_to : str ) -> None :
8498 """Extract a tar file with security checks against path traversal."""
8599 with tarfile .open (file_path , "r:*" ) as tar_ref :
100+ # Get absolute path of extract_to for proper comparison
101+ extract_to_abs = os .path .abspath (extract_to )
102+
86103 # Security: Check for path traversal in tar entries
87104 for member in tar_ref .getmembers ():
88- # Normalize the path and check for path traversal attempts
89- member_path = os .path .normpath (os .path .join (extract_to , member .name ))
90- if not member_path .startswith (os .path .abspath (extract_to )):
91- raise ValueError (f"Attempted path traversal in tar file: { member .name } " )
92- if ".." in member .name or member .name .startswith ("/" ):
105+ # Check for explicit path traversal attempts (../ or ..\ patterns)
106+ # Allow filenames containing ".." as a substring (e.g., "file..name.txt")
107+ if "../" in member .name or "..\\ " in member .name or member .name .startswith ("../" ) or member .name .startswith ("..\\ " ):
93108 raise ValueError (f"Invalid path in tar file: { member .name } " )
109+ # Check for absolute paths
110+ if member .name .startswith ("/" ) or (os .name == "nt" and len (member .name ) > 1 and member .name [1 ] == ":" ):
111+ raise ValueError (f"Invalid path in tar file: { member .name } " )
112+
113+ # Normalize the member path (remove leading slashes and normalize)
114+ member_normalized = os .path .normpath (member .name .lstrip ("/" ))
115+
116+ # Join with extract_to and normalize to get the full path
117+ member_path = os .path .normpath (os .path .join (extract_to_abs , member_normalized ))
118+
119+ # Check if the resolved path is within extract_to directory
120+ if not member_path .startswith (extract_to_abs + os .sep ) and member_path != extract_to_abs :
121+ raise ValueError (f"Attempted path traversal in tar file: { member .name } " )
94122
95123 tar_ref .extractall (extract_to )
96124 log .info (f"Extracted { file_path } to { extract_to } " )
0 commit comments