@@ -213,3 +213,77 @@ def test_is_not_extracted(self):
213213 extracted = self .trainer .is_extracted (self .trainer .data_path )
214214
215215 self .assertFalse (extracted )
216+
217+ def test_extract_raises_on_symlink_data_path (self ):
218+ """
219+ Test that extract() raises an exception when data_path is a symlink.
220+
221+ A local attacker could pre-plant a symlink at the predictable
222+ data_path location to redirect archive extraction to an arbitrary
223+ directory. The trainer must reject a symlink target before extracting.
224+ """
225+ import tempfile
226+ import shutil
227+
228+ attacker_target = tempfile .mkdtemp (prefix = 'cb_symlink_attack_' )
229+ try :
230+ file_object_path = self ._create_test_corpus (self ._get_data ())
231+
232+ # Plant the symlink at data_path before extraction
233+ os .makedirs (self .trainer .data_directory , exist_ok = True )
234+ os .symlink (attacker_target , self .trainer .data_path )
235+
236+ with self .assertRaises (Exception ):
237+ self .trainer .extract (file_object_path )
238+
239+ # Confirm nothing was written to the attacker's target directory
240+ self .assertEqual (
241+ os .listdir (attacker_target ), [],
242+ 'Files were written through the symlink to the attacker target directory'
243+ )
244+ finally :
245+ if os .path .islink (self .trainer .data_path ):
246+ os .unlink (self .trainer .data_path )
247+ shutil .rmtree (attacker_target , ignore_errors = True )
248+ self ._destroy_test_corpus ()
249+
250+ def test_extract_does_not_follow_symlink_members (self ):
251+ """
252+ Test that safe_extract() rejects tar members whose resolved paths
253+ escape the extraction directory via a symlink.
254+
255+ Even if data_path itself is legitimate, a tar archive containing a
256+ symlink member pointing outside the extraction root must be rejected.
257+ """
258+ import tempfile
259+ import shutil
260+
261+ attacker_target = tempfile .mkdtemp (prefix = 'cb_member_symlink_attack_' )
262+ try :
263+ os .makedirs (self .trainer .data_path , exist_ok = True )
264+
265+ # Build a tar containing a directory entry that is a symlink
266+ # pointing outside the extraction root, plus a file routed through it.
267+ file_path = os .path .join (self .trainer .data_directory , 'malicious.tgz' )
268+ with tarfile .TarFile (file_path , 'w' ) as tf :
269+ link_info = tarfile .TarInfo ('escape_link' )
270+ link_info .type = tarfile .SYMTYPE
271+ link_info .linkname = attacker_target
272+ tf .addfile (link_info )
273+
274+ payload = b'should not be written outside extraction root\n '
275+ file_info = tarfile .TarInfo ('escape_link/pwned.txt' )
276+ file_info .size = len (payload )
277+ tf .addfile (file_info , BytesIO (payload ))
278+
279+ with self .assertRaises (Exception ):
280+ self .trainer .extract (file_path )
281+
282+ self .assertEqual (
283+ os .listdir (attacker_target ), [],
284+ 'Files were written outside the extraction root via a symlink member'
285+ )
286+ finally :
287+ if os .path .exists (file_path ):
288+ os .remove (file_path )
289+ shutil .rmtree (attacker_target , ignore_errors = True )
0 commit comments