@@ -156,17 +156,15 @@ def test_check_zip_members_symlink_absolute_target():
156156
157157
158158def test_check_zip_members_symlink_dotdot_target ():
159- """ZIP symlink pointing outside extraction dir via .. must be rejected ."""
159+ """ZIP symlink with .. is allowed at pre-extraction time (post-copy check enforces boundary) ."""
160160 zf = _make_zip_with_symlink ("link" , "../../etc/shadow" )
161- with pytest .raises (RuntimeError , match = "symlink" ):
162- _check_zip_members (zf )
161+ _check_zip_members (zf ) # must NOT raise
163162
164163
165164def test_check_zip_members_symlink_windows_dotdot_target ():
166- """ZIP symlink with Windows-style backslash traversal must be rejected ."""
165+ """ZIP symlink with Windows-style backslash .. is allowed at pre-extraction time ."""
167166 zf = _make_zip_with_symlink ("link" , "..\\ ..\\ evil" )
168- with pytest .raises (RuntimeError , match = "symlink" ):
169- _check_zip_members (zf )
167+ _check_zip_members (zf ) # must NOT raise
170168
171169
172170def test_check_zip_members_symlink_safe_relative ():
@@ -269,18 +267,17 @@ def test_check_tar_member_type_absolute_symlink():
269267
270268
271269def test_check_tar_member_type_dotdot_symlink ():
270+ """Relative .. symlinks are allowed at pre-extraction time (post-copy check enforces boundary)."""
272271 tf = _make_tar_with_member (lambda t : _add_symlink (t , "link" , "../../etc/passwd" ))
273272 member = tf .getmembers ()[0 ]
274- with pytest .raises (RuntimeError , match = "unsafe target" ):
275- _check_tar_member_type (member )
273+ _check_tar_member_type (member ) # must NOT raise
276274
277275
278276def test_check_tar_member_type_windows_dotdot_symlink ():
279- """TAR symlink with Windows-style backslash traversal must be rejected ."""
277+ """Windows-style .. symlinks are allowed at pre-extraction time ."""
280278 tf = _make_tar_with_member (lambda t : _add_symlink (t , "link" , "..\\ ..\\ evil" ))
281279 member = tf .getmembers ()[0 ]
282- with pytest .raises (RuntimeError , match = "unsafe target" ):
283- _check_tar_member_type (member )
280+ _check_tar_member_type (member ) # must NOT raise
284281
285282
286283# ---------------------------------------------------------------------------
@@ -346,6 +343,49 @@ def test_check_tar_members_rejects_device_file():
346343 _check_tar_members (tf )
347344
348345
346+ # ---------------------------------------------------------------------------
347+ # ArchiveLocalRepo._check_symlinks_in_dest
348+ # ---------------------------------------------------------------------------
349+
350+
351+ def test_check_symlinks_in_dest_safe_internal_dotdot (tmp_path , monkeypatch ):
352+ """A symlink with .. that stays within dest_dir must be accepted."""
353+ monkeypatch .chdir (tmp_path )
354+ dest = tmp_path / "pkg"
355+ dest .mkdir ()
356+ (dest / "other" ).mkdir ()
357+ (dest / "other" / "target.mk" ).write_text ("content" )
358+ sub = dest / "sub" / "dir"
359+ sub .mkdir (parents = True )
360+ (sub / "link.mk" ).symlink_to ("../../other/target.mk" )
361+
362+ ArchiveLocalRepo ._check_symlinks_in_dest (str (dest )) # must NOT raise
363+
364+
365+ def test_check_symlinks_in_dest_escaping_symlink (tmp_path , monkeypatch ):
366+ """A symlink that resolves outside the manifest root must be rejected."""
367+ monkeypatch .chdir (tmp_path )
368+ dest = tmp_path / "pkg"
369+ dest .mkdir ()
370+ (dest / "evil.txt" ).symlink_to ("../../../etc/passwd" )
371+
372+ with pytest .raises (RuntimeError ):
373+ ArchiveLocalRepo ._check_symlinks_in_dest (str (dest ))
374+
375+
376+ def test_check_symlinks_in_dest_sibling_within_manifest (tmp_path , monkeypatch ):
377+ """A symlink pointing to a sibling project (still within manifest root) is accepted."""
378+ monkeypatch .chdir (tmp_path )
379+ sibling = tmp_path / "sibling"
380+ sibling .mkdir ()
381+ (sibling / "file.txt" ).write_text ("content" )
382+ dest = tmp_path / "pkg"
383+ dest .mkdir ()
384+ (dest / "link.txt" ).symlink_to ("../sibling/file.txt" )
385+
386+ ArchiveLocalRepo ._check_symlinks_in_dest (str (dest )) # must NOT raise
387+
388+
349389# ---------------------------------------------------------------------------
350390# ArchiveRemote.is_accessible
351391# ---------------------------------------------------------------------------
0 commit comments