From: Dustin L. Howett Date: Fri, 26 Jun 2026 14:46:23 +0000 (-0500) Subject: test: add a variant of the secure tests for windows X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=56546a0a4e35f5e9b80a9b5ac1aee09ddd417e22;p=thirdparty%2Flibarchive.git test: add a variant of the secure tests for windows --- diff --git a/libarchive/test/test_write_disk_secure.c b/libarchive/test/test_write_disk_secure.c index ddaf95d74..e64117d4f 100644 --- a/libarchive/test/test_write_disk_secure.c +++ b/libarchive/test/test_write_disk_secure.c @@ -26,6 +26,10 @@ #define UMASK 022 +#ifndef S_IFLNK +#define S_IFLNK 0120000 +#endif + /* * Exercise security checks that should prevent certain * writes. @@ -304,3 +308,232 @@ DEFINE_TEST(test_write_disk_secure) assert(0 != lstat("dir/filed", &st)); #endif } + +/* + * This is a simplified variant of the above test which never turns off secure + * symlinks. It is designed to test quirks in the Windows implementation of + * archive_write_disk; however, its behavior under test should not be exclusive + * to Windows. + */ +DEFINE_TEST(test_write_disk_secure_symlinks_only) +{ + struct archive *a; + struct archive_entry *ae; + const int default_options = ARCHIVE_EXTRACT_SECURE_SYMLINKS; + + if (!canSymlink()) { + skipping("Can't test symlinks on this filesystem"); + return; + } + + /* Start with a known umask. */ + assertUmask(UMASK); + + /* Create an archive_write_disk object. */ + assert((a = archive_write_disk_new()) != NULL); + archive_write_disk_set_options(a, default_options); + + /* Write a regular dir to it. */ + assert((ae = archive_entry_new()) != NULL); + archive_entry_copy_pathname(ae, "dir"); + archive_entry_set_mode(ae, S_IFDIR | 0777); + assert(0 == archive_write_header(a, ae)); + archive_entry_free(ae); + assert(0 == archive_write_finish_entry(a)); + assertIsDir("dir", -1); + + /* Write a symlink to the dir above. */ + assert((ae = archive_entry_new()) != NULL); + archive_entry_copy_pathname(ae, "link_to_dir"); + archive_entry_set_mode(ae, S_IFLNK | 0777); + archive_entry_set_symlink(ae, "dir"); + archive_entry_set_symlink_type(ae, AE_SYMLINK_TYPE_DIRECTORY); + assert(0 == archive_write_header(a, ae)); + assert(0 == archive_write_finish_entry(a)); + assertIsSymlink("link_to_dir", "dir", 1); + + /* With security checks enabled, this should fail. */ + assert(archive_entry_clear(ae) != NULL); + archive_entry_copy_pathname(ae, "link_to_dir/fileb"); + archive_entry_set_mode(ae, S_IFREG | 0777); + failure("Extracting a file through a symlink should fail here."); + assertEqualInt(ARCHIVE_FAILED, archive_write_header(a, ae)); + archive_entry_free(ae); + assert(0 == archive_write_finish_entry(a)); + assertFileNotExists("dir/fileb"); + + /* Create another link. */ + assert((ae = archive_entry_new()) != NULL); + archive_entry_copy_pathname(ae, "link_to_dir2"); + archive_entry_set_mode(ae, S_IFLNK | 0777); + archive_entry_set_symlink(ae, "dir"); + archive_entry_set_symlink_type(ae, AE_SYMLINK_TYPE_DIRECTORY); + assert(0 == archive_write_header(a, ae)); + assert(0 == archive_write_finish_entry(a)); + assertIsSymlink("link_to_dir2", "dir", 1); + + /* + * With symlink check and unlink option, it should remove + * the link and create the dir. + */ + assert(archive_entry_clear(ae) != NULL); + archive_entry_copy_pathname(ae, "link_to_dir2/filec"); + archive_entry_set_mode(ae, S_IFREG | 0777); + archive_write_disk_set_options(a, default_options | ARCHIVE_EXTRACT_UNLINK); + assertEqualIntA(a, ARCHIVE_OK, archive_write_header(a, ae)); + archive_entry_free(ae); + assert(0 == archive_write_finish_entry(a)); + assertIsDir("link_to_dir2", -1); + assertIsReg("link_to_dir2/filec", -1); + + /* + * Restore the prior security mode. + */ + archive_write_disk_set_options(a, default_options); + + /* Create a nested symlink. */ + assert((ae = archive_entry_new()) != NULL); + archive_entry_copy_pathname(ae, "dir/nested_link_to_dir"); + archive_entry_set_mode(ae, S_IFLNK | 0777); + archive_entry_set_symlink(ae, "../dir"); + archive_entry_set_symlink_type(ae, AE_SYMLINK_TYPE_DIRECTORY); + assert(0 == archive_write_header(a, ae)); + assert(0 == archive_write_finish_entry(a)); + assertIsSymlink("dir/nested_link_to_dir", "../dir", -1); + + /* With security checks enabled, this should fail. */ + assert(archive_entry_clear(ae) != NULL); + archive_entry_copy_pathname(ae, "dir/nested_link_to_dir/filed"); + archive_entry_set_mode(ae, S_IFREG | 0777); + failure("Extracting a file through a symlink should fail here."); + assertEqualInt(ARCHIVE_FAILED, archive_write_header(a, ae)); + archive_entry_free(ae); + assert(0 == archive_write_finish_entry(a)); + assertFileNotExists("dir/filed"); + + /* Create a symlink to a dir. */ + assert((ae = archive_entry_new()) != NULL); + archive_entry_copy_pathname(ae, "link_to_dir3"); + archive_entry_set_mode(ae, S_IFLNK | 0777); + archive_entry_set_symlink(ae, "dir"); + archive_entry_set_symlink_type(ae, AE_SYMLINK_TYPE_DIRECTORY); + assert(0 == archive_write_header(a, ae)); + assert(0 == archive_write_finish_entry(a)); + assertIsSymlink("link_to_dir3", "dir", 1); + /* Extract a dir whose name matches the symlink. */ + assert(archive_entry_clear(ae) != NULL); + archive_entry_copy_pathname(ae, "link_to_dir3"); + archive_entry_set_mode(ae, S_IFDIR | 0777); + assert(0 == archive_write_header(a, ae)); + assert(0 == archive_write_finish_entry(a)); + archive_entry_free(ae); + assertIsDir("link_to_dir3", -1); + + /* + * As above, but a broken link, so the link should get replaced. + */ + + /* Create a symlink to a dir. */ + assert((ae = archive_entry_new()) != NULL); + archive_entry_copy_pathname(ae, "link_to_dir4"); + archive_entry_set_mode(ae, S_IFLNK | 0777); + archive_entry_set_symlink(ae, "nonexistent_dir"); + archive_entry_set_symlink_type(ae, AE_SYMLINK_TYPE_DIRECTORY); + assert(0 == archive_write_header(a, ae)); + assert(0 == archive_write_finish_entry(a)); + assertIsSymlink("link_to_dir4", "nonexistent_dir", 1); + /* Extract a dir whose name matches the symlink. */ + assert(archive_entry_clear(ae) != NULL); + archive_entry_copy_pathname(ae, "link_to_dir4"); + archive_entry_set_mode(ae, S_IFDIR | 0777); + assert(0 == archive_write_header(a, ae)); + assert(0 == archive_write_finish_entry(a)); + archive_entry_free(ae); + assertIsDir("link_to_dir4", -1); + + /* + * As above, but a link to a non-dir, so the link should get replaced. + * (file is named "link_to_dir" because we are transforming a link into a dir,) + */ + /* Create a regular file and a symlink to it */ + assert((ae = archive_entry_new()) != NULL); + archive_entry_copy_pathname(ae, "non_dir"); + archive_entry_set_mode(ae, S_IFREG | 0777); + assert(0 == archive_write_header(a, ae)); + assert(0 == archive_write_finish_entry(a)); + assertIsReg("non_dir", -1); + /* Create symlink to the file. */ + archive_entry_copy_pathname(ae, "link_to_dir5"); + archive_entry_set_mode(ae, S_IFLNK | 0777); + archive_entry_set_symlink(ae, "non_dir"); + archive_entry_set_symlink_type(ae, AE_SYMLINK_TYPE_FILE); + assert(0 == archive_write_header(a, ae)); + assert(0 == archive_write_finish_entry(a)); + assertIsSymlink("link_to_dir5", "non_dir", 0); + /* Extract a dir whose name matches the symlink. */ + assert(archive_entry_clear(ae) != NULL); + archive_entry_copy_pathname(ae, "link_to_dir5"); + archive_entry_set_mode(ae, S_IFDIR | 0777); + assert(0 == archive_write_header(a, ae)); + assert(0 == archive_write_finish_entry(a)); + archive_entry_free(ae); + assertIsDir("link_to_dir5", -1); + + /* + * Create a link to a (technically safe) directory, then replace it, then write through it. + * Exercises the safety cache to ensure that it does not treat new entries as safe. + */ + assert((ae = archive_entry_new()) != NULL); + archive_entry_copy_pathname(ae, "link_to_dir6"); + archive_entry_set_mode(ae, S_IFLNK | 0777); + archive_entry_set_symlink(ae, "dir"); + archive_entry_set_symlink_type(ae, AE_SYMLINK_TYPE_DIRECTORY); + assert(0 == archive_write_header(a, ae)); + assert(0 == archive_write_finish_entry(a)); + assertIsSymlink("link_to_dir6", "dir", 1); + /* Replace it. */ + assert(archive_entry_clear(ae) != NULL); + archive_entry_copy_pathname(ae, "link_to_dir6"); + archive_entry_set_mode(ae, S_IFLNK | 0777); + archive_entry_set_symlink(ae, "dir"); + archive_entry_set_symlink_type(ae, AE_SYMLINK_TYPE_DIRECTORY); + assert(0 == archive_write_header(a, ae)); + assert(0 == archive_write_finish_entry(a)); + assertIsSymlink("link_to_dir6", "dir", 1); + /* Extract through it. */ + assert(archive_entry_clear(ae) != NULL); + archive_entry_copy_pathname(ae, "link_to_dir6/filee"); + archive_entry_set_mode(ae, S_IFREG | 0777); + assertEqualInt(ARCHIVE_FAILED, archive_write_header(a, ae)); + archive_entry_free(ae); + assertFileNotExists("dir/filee"); + + /* + * Create an empty directory, then replace it, then write through it. + * Exercises the safety cache to ensure that it does not treat new entries as safe. + */ + assert((ae = archive_entry_new()) != NULL); + archive_entry_copy_pathname(ae, "link_to_dir7"); + archive_entry_set_mode(ae, S_IFDIR | 0777); + assert(0 == archive_write_header(a, ae)); + assert(0 == archive_write_finish_entry(a)); + assertIsDir("link_to_dir7", -1); + /* Replace it. */ + assert(archive_entry_clear(ae) != NULL); + archive_entry_copy_pathname(ae, "link_to_dir7"); + archive_entry_set_mode(ae, S_IFLNK | 0777); + archive_entry_set_symlink(ae, "dir"); + archive_entry_set_symlink_type(ae, AE_SYMLINK_TYPE_DIRECTORY); + assert(0 == archive_write_header(a, ae)); + assert(0 == archive_write_finish_entry(a)); + assertIsSymlink("link_to_dir7", "dir", 1); + /* Extract through it. */ + assert(archive_entry_clear(ae) != NULL); + archive_entry_copy_pathname(ae, "link_to_dir7/filef"); + archive_entry_set_mode(ae, S_IFREG | 0777); + assertEqualInt(ARCHIVE_FAILED, archive_write_header(a, ae)); + archive_entry_free(ae); + assertFileNotExists("dir/filef"); + + assertEqualInt(ARCHIVE_OK, archive_write_free(a)); +}