[git commit] libarchive: do not extract unsafe symlinks unless $EXTRACT_UNSAFE_SYMLINKS=1

Denys Vlasenko vda.linux at googlemail.com
Thu Aug 10 09:52:42 UTC 2017


commit: https://git.busybox.net/busybox/commit/?id=bc9bbeb2b81001e8731cd2ae501c8fccc8d87cc7
branch: https://git.busybox.net/busybox/commit/?id=refs/heads/master

function                                             old     new   delta
unsafe_symlink_target                                  -     147    +147
unzip_main                                          2711    2732     +21
copy_file                                           1657    1678     +21
tar_main                                             999     971     -28
data_extract_all                                    1038     984     -54
------------------------------------------------------------------------------
(add/remove: 2/0 grow/shrink: 2/2 up/down: 189/-82)           Total: 107 bytes

Signed-off-by: Denys Vlasenko <vda.linux at googlemail.com>
---
 archival/libarchive/Kbuild.src              |  2 ++
 archival/libarchive/data_extract_all.c      | 37 +++++++++-------------
 archival/libarchive/unsafe_symlink_target.c | 48 +++++++++++++++++++++++++++++
 archival/tar.c                              | 19 ------------
 archival/unzip.c                            | 10 ++++--
 coreutils/link.c                            |  5 ++-
 include/bb_archive.h                        |  4 +--
 libbb/copy_file.c                           |  5 ++-
 testsuite/tar.tests                         | 10 +++---
 9 files changed, 85 insertions(+), 55 deletions(-)

diff --git a/archival/libarchive/Kbuild.src b/archival/libarchive/Kbuild.src
index 942e755..e1a8a75 100644
--- a/archival/libarchive/Kbuild.src
+++ b/archival/libarchive/Kbuild.src
@@ -12,6 +12,8 @@ COMMON_FILES:= \
 	data_extract_all.o \
 	data_extract_to_stdout.o \
 \
+	unsafe_symlink_target.o \
+\
 	filter_accept_all.o \
 	filter_accept_list.o \
 	filter_accept_reject_list.o \
diff --git a/archival/libarchive/data_extract_all.c b/archival/libarchive/data_extract_all.c
index 1ce927c..e658444 100644
--- a/archival/libarchive/data_extract_all.c
+++ b/archival/libarchive/data_extract_all.c
@@ -129,9 +129,7 @@ void FAST_FUNC data_extract_all(archive_handle_t *archive_handle)
 		if (res != 0 && !(archive_handle->ah_flags & ARCHIVE_EXTRACT_QUIET)) {
 			/* shared message */
 			bb_perror_msg("can't create %slink '%s' to '%s'",
-					"hard",
-					dst_name,
-					hard_link
+				"hard",	dst_name, hard_link
 			);
 		}
 		/* Hardlinks have no separate mode/ownership, skip chown/chmod */
@@ -181,7 +179,9 @@ void FAST_FUNC data_extract_all(archive_handle_t *archive_handle)
 //TODO: what if file_header->link_target == NULL (say, corrupted tarball?)
 
 		/* To avoid a directory traversal attack via symlinks,
-		 * for certain link targets postpone creation of symlinks.
+		 * do not restore symlinks with ".." components
+		 * or symlinks starting with "/", unless a magic
+		 * envvar is set.
 		 *
 		 * For example, consider a .tar created via:
 		 *  $ tar cvf bug.tar anything.txt
@@ -199,24 +199,17 @@ void FAST_FUNC data_extract_all(archive_handle_t *archive_handle)
 		 *
 		 * Untarring bug.tar would otherwise place evil.py in '/tmp'.
 		 */
-		if (file_header->link_target[0] == '/'
-		 || strstr(file_header->link_target, "..")
-		) {
-			llist_add_to(&archive_handle->symlink_placeholders,
-				xasprintf("%s%c%s", file_header->name, '\0', file_header->link_target)
-			);
-			break;
-		}
-		res = symlink(file_header->link_target, dst_name);
-		if (res != 0
-		 && !(archive_handle->ah_flags & ARCHIVE_EXTRACT_QUIET)
-		) {
-			/* shared message */
-			bb_perror_msg("can't create %slink '%s' to '%s'",
-				"sym",
-				dst_name,
-				file_header->link_target
-			);
+		if (!unsafe_symlink_target(file_header->link_target)) {
+			res = symlink(file_header->link_target, dst_name);
+			if (res != 0
+			 && !(archive_handle->ah_flags & ARCHIVE_EXTRACT_QUIET)
+			) {
+				/* shared message */
+				bb_perror_msg("can't create %slink '%s' to '%s'",
+					"sym",
+					dst_name, file_header->link_target
+				);
+			}
 		}
 		break;
 	case S_IFSOCK:
diff --git a/archival/libarchive/unsafe_symlink_target.c b/archival/libarchive/unsafe_symlink_target.c
new file mode 100644
index 0000000..441ba8b
--- /dev/null
+++ b/archival/libarchive/unsafe_symlink_target.c
@@ -0,0 +1,48 @@
+/* vi: set sw=4 ts=4: */
+/*
+ * Licensed under GPLv2 or later, see file LICENSE in this source tree.
+ */
+#include "libbb.h"
+#include "bb_archive.h"
+
+int FAST_FUNC unsafe_symlink_target(const char *target)
+{
+	const char *dot;
+
+	if (target[0] == '/') {
+		const char *var;
+ unsafe:
+		var = getenv("EXTRACT_UNSAFE_SYMLINKS");
+		if (var) {
+			if (LONE_CHAR(var, '1'))
+				return 0; /* pretend it's safe */
+			return 1; /* "UNSAFE!" */
+		}
+		bb_error_msg("skipping unsafe symlink to '%s' in archive,"
+			" set %s=1 to extract",
+			target,
+			"EXTRACT_UNSAFE_SYMLINKS"
+		);
+		/* Prevent further messages */
+		setenv("EXTRACT_UNSAFE_SYMLINKS", "0", 0);
+		return 1; /* "UNSAFE!" */
+	}
+
+	dot = target;
+	for (;;) {
+		dot = strchr(dot, '.');
+		if (!dot)
+			return 0; /* safe target */
+
+		/* Is it a path component starting with ".."? */
+		if ((dot[1] == '.')
+		 && (dot == target || dot[-1] == '/')
+		    /* Is it exactly ".."? */
+		 && (dot[2] == '/' || dot[2] == '\0')
+		) {
+			goto unsafe;
+		}
+		/* NB: it can even be trailing ".", should only add 1 */
+		dot += 1;
+	}
+}
diff --git a/archival/tar.c b/archival/tar.c
index 73c14ca..9a5bcc7 100644
--- a/archival/tar.c
+++ b/archival/tar.c
@@ -278,23 +278,6 @@ static void chksum_and_xwrite(int fd, struct tar_header_t* hp)
 	xwrite(fd, hp, sizeof(*hp));
 }
 
-static void replace_symlink_placeholders(llist_t *list)
-{
-	while (list) {
-		char *target;
-
-		target = list->data + strlen(list->data) + 1;
-		if (symlink(target, list->data)) {
-			/* shared message */
-			bb_error_msg_and_die("can't create %slink '%s' to '%s'",
-				"sym",
-				list->data, target
-			);
-		}
-		list = list->link;
-	}
-}
-
 #if ENABLE_FEATURE_TAR_GNU_EXTENSIONS
 static void writeLongname(int fd, int type, const char *name, int dir)
 {
@@ -1255,8 +1238,6 @@ int tar_main(int argc UNUSED_PARAM, char **argv)
 	while (get_header_tar(tar_handle) == EXIT_SUCCESS)
 		bb_got_signal = EXIT_SUCCESS; /* saw at least one header, good */
 
-	replace_symlink_placeholders(tar_handle->symlink_placeholders);
-
 	/* Check that every file that should have been extracted was */
 	while (tar_handle->accept) {
 		if (!find_list_entry(tar_handle->reject, tar_handle->accept->data)
diff --git a/archival/unzip.c b/archival/unzip.c
index 8ed9ae7..6041660 100644
--- a/archival/unzip.c
+++ b/archival/unzip.c
@@ -368,9 +368,15 @@ static void unzip_extract_symlink(zip_header_t *zip, const char *dst_fn)
 		target[xstate.mem_output_size] = '\0';
 #endif
 	}
+	if (!unsafe_symlink_target(target)) {
 //TODO: libbb candidate
-	if (symlink(target, dst_fn))
-		bb_perror_msg_and_die("can't create symlink '%s'", dst_fn);
+		if (symlink(target, dst_fn)) {
+			/* shared message */
+			bb_perror_msg_and_die("can't create %slink '%s' to '%s'",
+				"sym", dst_fn, target
+			);
+		}
+	}
 	free(target);
 }
 #endif
diff --git a/coreutils/link.c b/coreutils/link.c
index 81808b7..d8d583b 100644
--- a/coreutils/link.c
+++ b/coreutils/link.c
@@ -31,9 +31,8 @@ int link_main(int argc UNUSED_PARAM, char **argv)
 	argv += optind;
 	if (link(argv[0], argv[1]) != 0) {
 		/* shared message */
-		bb_perror_msg_and_die("can't create %slink "
-					"'%s' to '%s'", "hard",
-					argv[1], argv[0]
+		bb_perror_msg_and_die("can't create %slink '%s' to '%s'",
+			"hard",	argv[1], argv[0]
 		);
 	}
 	return EXIT_SUCCESS;
diff --git a/include/bb_archive.h b/include/bb_archive.h
index d376241..d3a02cf 100644
--- a/include/bb_archive.h
+++ b/include/bb_archive.h
@@ -64,9 +64,6 @@ typedef struct archive_handle_t {
 	/* Currently processed file's header */
 	file_header_t *file_header;
 
-	/* List of symlink placeholders */
-	llist_t *symlink_placeholders;
-
 	/* Process the header component, e.g. tar -t */
 	void FAST_FUNC (*action_header)(const file_header_t *);
 
@@ -200,6 +197,7 @@ void seek_by_jump(int fd, off_t amount) FAST_FUNC;
 void seek_by_read(int fd, off_t amount) FAST_FUNC;
 
 const char *strip_unsafe_prefix(const char *str) FAST_FUNC;
+int unsafe_symlink_target(const char *target) FAST_FUNC;
 
 void data_align(archive_handle_t *archive_handle, unsigned boundary) FAST_FUNC;
 const llist_t *find_list_entry(const llist_t *list, const char *filename) FAST_FUNC;
diff --git a/libbb/copy_file.c b/libbb/copy_file.c
index 23c0f83..be90066 100644
--- a/libbb/copy_file.c
+++ b/libbb/copy_file.c
@@ -371,7 +371,10 @@ int FAST_FUNC copy_file(const char *source, const char *dest, int flags)
 			int r = symlink(lpath, dest);
 			free(lpath);
 			if (r < 0) {
-				bb_perror_msg("can't create symlink '%s'", dest);
+				/* shared message */
+				bb_perror_msg("can't create %slink '%s' to '%s'",
+					"sym", dest, lpath
+				);
 				return -1;
 			}
 			if (flags & FILEUTILS_PRESERVE_STATUS)
diff --git a/testsuite/tar.tests b/testsuite/tar.tests
index 1675b07..b7cd74c 100755
--- a/testsuite/tar.tests
+++ b/testsuite/tar.tests
@@ -279,7 +279,7 @@ optional UUDECODE FEATURE_TAR_AUTODETECT FEATURE_SEAMLESS_BZ2
 testing "tar does not extract into symlinks" "\
 >>/tmp/passwd && uudecode -o input && tar xf input 2>&1 && rm passwd; cat /tmp/passwd; echo \$?
 " "\
-tar: can't create symlink 'passwd' to '/tmp/passwd'
+tar: skipping unsafe symlink to '/tmp/passwd' in archive, set EXTRACT_UNSAFE_SYMLINKS=1 to extract
 0
 " \
 "" "\
@@ -299,7 +299,7 @@ optional UUDECODE FEATURE_TAR_AUTODETECT FEATURE_SEAMLESS_BZ2
 testing "tar -k does not extract into symlinks" "\
 >>/tmp/passwd && uudecode -o input && tar xf input -k 2>&1 && rm passwd; cat /tmp/passwd; echo \$?
 " "\
-tar: can't create symlink 'passwd' to '/tmp/passwd'
+tar: skipping unsafe symlink to '/tmp/passwd' in archive, set EXTRACT_UNSAFE_SYMLINKS=1 to extract
 0
 " \
 "" "\
@@ -324,11 +324,11 @@ rm -rf etc usr
 ' "\
 etc/ssl/certs/3b2716e5.0
 etc/ssl/certs/EBG_Elektronik_Sertifika_Hizmet_Sağlayıcısı.pem
+tar: skipping unsafe symlink to '/usr/share/ca-certificates/mozilla/EBG_Elektronik_Sertifika_Hizmet_Sağlayıcısı.crt' in archive, set EXTRACT_UNSAFE_SYMLINKS=1 to extract
 etc/ssl/certs/f80cc7f6.0
 usr/share/ca-certificates/mozilla/EBG_Elektronik_Sertifika_Hizmet_Sağlayıcısı.crt
 0
 etc/ssl/certs/3b2716e5.0 -> EBG_Elektronik_Sertifika_Hizmet_Sağlayıcısı.pem
-etc/ssl/certs/EBG_Elektronik_Sertifika_Hizmet_Sağlayıcısı.pem -> /usr/share/ca-certificates/mozilla/EBG_Elektronik_Sertifika_Hizmet_Sağlayıcısı.crt
 etc/ssl/certs/f80cc7f6.0 -> EBG_Elektronik_Sertifika_Hizmet_Sağlayıcısı.pem
 " \
 "" ""
@@ -346,9 +346,9 @@ ls symlink/bb_test_evilfile
 ' "\
 anything.txt
 symlink
+tar: skipping unsafe symlink to '/tmp' in archive, set EXTRACT_UNSAFE_SYMLINKS=1 to extract
 symlink/bb_test_evilfile
-tar: can't create symlink 'symlink' to '/tmp'
-1
+0
 ls: /tmp/bb_test_evilfile: No such file or directory
 ls: bb_test_evilfile: No such file or directory
 symlink/bb_test_evilfile


More information about the busybox-cvs mailing list