@@ -108,8 +108,7 @@ def test_extract_tarfile_file_name(self, is_within_directory_patch, tarfile_open
108108 extract_tarfile (tarfile_path = tarfile_path , unpack_dir = unpack_dir )
109109
110110 is_within_directory_patch .assert_called_once ()
111- tarfile_file_mock .getmembers .assert_called_once ()
112- tarfile_file_mock .extractall .assert_called_once_with (unpack_dir )
111+ tarfile_file_mock .extractall .assert_called_once_with (path = unpack_dir , filter = "data" )
113112
114113 @patch ("samcli.lib.utils.tar.tarfile.open" )
115114 @patch ("samcli.lib.utils.tar._is_within_directory" )
@@ -118,8 +117,8 @@ def test_extract_tarfile_fileobj(self, is_within_directory_patch, tarfile_open_p
118117 unpack_dir = "/test_unpack_dir/"
119118 is_within_directory_patch .return_value = True
120119
121- tarfile_file_mock = Mock () # Mock tarfile
122- tar_file_obj_mock = Mock () # Mock member inside tarfile
120+ tarfile_file_mock = Mock ()
121+ tar_file_obj_mock = Mock ()
123122 tar_file_obj_mock .name = "obj_name"
124123 tarfile_file_mock .getmembers .return_value = [tar_file_obj_mock ]
125124 tarfile_open_patch .return_value .__enter__ .return_value = tarfile_file_mock
@@ -128,7 +127,7 @@ def test_extract_tarfile_fileobj(self, is_within_directory_patch, tarfile_open_p
128127
129128 is_within_directory_patch .assert_called_once ()
130129 tarfile_file_mock .getmembers .assert_called_once ()
131- tarfile_file_mock .extractall .assert_called_once_with (unpack_dir )
130+ tarfile_file_mock .extractall .assert_called_once_with (path = unpack_dir , filter = "data" )
132131
133132 @patch ("samcli.lib.utils.tar.tarfile.open" )
134133 @patch ("samcli.lib.utils.tar._is_within_directory" )
@@ -140,6 +139,7 @@ def test_extract_tarfile_obj_not_within_dir(self, is_within_directory_patch, tar
140139 tarfile_file_mock = Mock ()
141140 tar_file_obj_mock = Mock ()
142141 tar_file_obj_mock .name = "obj_name"
142+ tar_file_obj_mock .issym .return_value = False
143143 tarfile_file_mock .getmembers .return_value = [tar_file_obj_mock ]
144144 tarfile_open_patch .return_value .__enter__ .return_value = tarfile_file_mock
145145
@@ -231,3 +231,104 @@ def test_validating_symlinked_tar_path_directory(self, file_exists, path_mock):
231231 result = _validate_destinations_exists (["mock_folder" ])
232232
233233 self .assertEqual (result , file_exists )
234+
235+ @patch ("samcli.lib.utils.tar.tarfile.open" )
236+ @patch ("samcli.lib.utils.tar._is_within_directory" )
237+ def test_extract_tarfile_allows_safe_symlinks (self , is_within_directory_patch , tarfile_open_patch ):
238+ """When mount_symlinks is False (default), filter='data' is used."""
239+ tarfile_path = "/test_tarfile_path/"
240+ unpack_dir = "/test_unpack_dir/"
241+ is_within_directory_patch .return_value = True
242+
243+ tarfile_file_mock = Mock ()
244+
245+ regular_file_1 = Mock ()
246+ regular_file_1 .name = "regular_file_1.txt"
247+
248+ safe_symlink = Mock ()
249+ safe_symlink .name = "safe_symlink"
250+
251+ tarfile_file_mock .getmembers .return_value = [regular_file_1 , safe_symlink ]
252+ tarfile_open_patch .return_value .__enter__ .return_value = tarfile_file_mock
253+
254+ extract_tarfile (tarfile_path = tarfile_path , unpack_dir = unpack_dir )
255+
256+ tarfile_file_mock .extractall .assert_called_once_with (path = unpack_dir , filter = "data" )
257+
258+ @patch ("samcli.lib.utils.tar.tarfile.open" )
259+ @patch ("samcli.lib.utils.tar._is_within_directory" )
260+ def test_extract_tarfile_skips_unsafe_symlinks (self , is_within_directory_patch , tarfile_open_patch ):
261+ """When mount_symlinks is False (default), filter='data' handles symlink safety."""
262+ tarfile_path = "/test_tarfile_path/"
263+ unpack_dir = "/test_unpack_dir/"
264+ is_within_directory_patch .return_value = True
265+
266+ tarfile_file_mock = Mock ()
267+
268+ regular_file_1 = Mock ()
269+ regular_file_1 .name = "regular_file_1.txt"
270+
271+ unsafe_symlink = Mock ()
272+ unsafe_symlink .name = "unsafe_symlink"
273+
274+ tarfile_file_mock .getmembers .return_value = [regular_file_1 , unsafe_symlink ]
275+ tarfile_open_patch .return_value .__enter__ .return_value = tarfile_file_mock
276+
277+ extract_tarfile (tarfile_path = tarfile_path , unpack_dir = unpack_dir )
278+
279+ # filter='data' is passed to extractall to handle symlink filtering
280+ tarfile_file_mock .extractall .assert_called_once_with (path = unpack_dir , filter = "data" )
281+
282+ @patch ("samcli.lib.utils.tar.tarfile.open" )
283+ @patch ("samcli.lib.utils.tar._is_within_directory" )
284+ def test_extract_tarfile_allows_outside_symlinks_when_mount_symlinks_enabled (
285+ self , is_within_directory_patch , tarfile_open_patch
286+ ):
287+ """When mount_symlinks is True, no filter is applied so symlinks are preserved."""
288+ tarfile_path = "/test_tarfile_path/"
289+ unpack_dir = "/test_unpack_dir/"
290+ is_within_directory_patch .return_value = True
291+
292+ tarfile_file_mock = Mock ()
293+
294+ regular_file = Mock ()
295+ regular_file .name = "regular_file.txt"
296+
297+ outside_symlink = Mock ()
298+ outside_symlink .name = "outside_symlink"
299+
300+ tarfile_file_mock .getmembers .return_value = [regular_file , outside_symlink ]
301+ tarfile_open_patch .return_value .__enter__ .return_value = tarfile_file_mock
302+
303+ extract_tarfile (tarfile_path = tarfile_path , unpack_dir = unpack_dir , mount_symlinks = True )
304+
305+ # No filter applied — symlinks pointing outside are allowed
306+ tarfile_file_mock .extractall .assert_called_once_with (path = unpack_dir )
307+
308+ @patch ("samcli.lib.utils.tar.tarfile.open" )
309+ @patch ("samcli.lib.utils.tar._is_within_directory" )
310+ def test_extract_tarfile_raises_when_data_filter_unavailable (self , is_within_directory_patch , tarfile_open_patch ):
311+ """When data_filter is not available, raises ExtractError."""
312+ tarfile_path = "/test_tarfile_path/"
313+ unpack_dir = "/test_unpack_dir/"
314+ is_within_directory_patch .return_value = True
315+
316+ tarfile_file_mock = Mock ()
317+ tar_file_obj_mock = Mock ()
318+ tar_file_obj_mock .name = "obj_name"
319+ tarfile_file_mock .getmembers .return_value = [tar_file_obj_mock ]
320+ tarfile_open_patch .return_value .__enter__ .return_value = tarfile_file_mock
321+
322+ # Temporarily remove data_filter to simulate older Python
323+ data_filter = getattr (tarfile , "data_filter" , None )
324+ try :
325+ if hasattr (tarfile , "data_filter" ):
326+ delattr (tarfile , "data_filter" )
327+
328+ with self .assertRaises (tarfile .ExtractError ) as ctx :
329+ extract_tarfile (tarfile_path = tarfile_path , unpack_dir = unpack_dir )
330+
331+ self .assertIn ("does not support tarfile extraction filters" , str (ctx .exception ))
332+ finally :
333+ if data_filter is not None :
334+ tarfile .data_filter = data_filter
0 commit comments