From: Denys Vlasenko <vda.linux@googlemail.com>
Date: Thu Aug 10 11:52:42 2017 +0200
Subject: libarchive: do not extract unsafe symlinks unless $EXTRACT_UNSAFE_SYMLINKS=1
Patch-mainline: bc9bbeb2b81001e8731cd2ae501c8fccc8d87cc7
Git-repo: https://git.busybox.net/busybox
Git-commit: 1c7f0c2bf03ff0f95bdfdbec70a6fbd92f08e8e5
References: 

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@googlemail.com>
Signed-off-by: Egbert Eich <eich@suse.de>
---
 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 ++++--
 include/bb_archive.h                        |  4 +--
 libbb/copy_file.c                           |  5 ++-
 testsuite/tar.tests                         | 10 +++---
 8 files changed, 83 insertions(+), 52 deletions(-)
diff --git a/archival/libarchive/Kbuild.src b/archival/libarchive/Kbuild.src
index 84117fa85..6310d540e 100644
--- a/archival/libarchive/Kbuild.src
+++ b/archival/libarchive/Kbuild.src
@@ -11,6 +11,8 @@ COMMON_FILES:= \
 	data_skip.o \
 	data_extract_all.o \
 	data_extract_to_stdout.o \
+\
+	unsafe_symlink_target.o \
 \
 	filter_accept_all.o \
 	filter_accept_list.o \
diff --git a/archival/libarchive/data_extract_all.c b/archival/libarchive/data_extract_all.c
index 1ce927c2f..e658444e0 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 000000000..441ba8b24
--- /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 fe193ba7e..8e3002ab4 100644
--- a/archival/tar.c
+++ b/archival/tar.c
@@ -289,23 +289,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)
 {
@@ -1257,8 +1240,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 105d725f0..97a1a031d 100644
--- a/archival/unzip.c
+++ b/archival/unzip.c
@@ -355,9 +355,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);
 }
 
diff --git a/include/bb_archive.h b/include/bb_archive.h
index d3762415f..d3a02cf18 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 23c0f8320..be9006631 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 1675b07b1..b7cd74ca5 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
