From 3ab1d6c123a6916e7efb821a441164ae56c6cd01 Mon Sep 17 00:00:00 2001
From: Denys Vlasenko <vda.linux@googlemail.com>
Date: Thu, 29 Jan 2026 11:48:02 +0100
Subject: [PATCH] tar: strip unsafe hardlink components - GNU tar does the same

Defends against files like these (python reproducer):

import tarfile
ti = tarfile.TarInfo("leak_hosts")
ti.type = tarfile.LNKTYPE
ti.linkname = "/etc/hosts"  # or "../etc/hosts" or ".."
ti.size = 0
with tarfile.open("/tmp/hardlink.tar", "w") as t:
	t.addfile(ti)

function                                             old     new   delta
skip_unsafe_prefix                                     -     127    +127
get_header_tar                                      1752    1754      +2
.rodata                                           106861  106856      -5
unzip_main                                          2715    2706      -9
strip_unsafe_prefix                                  102      18     -84
------------------------------------------------------------------------------
(add/remove: 1/0 grow/shrink: 1/3 up/down: 129/-98)            Total: 31 bytes

Signed-off-by: Denys Vlasenko <vda.linux@googlemail.com>

CVE: CVE-2026-26157
CVE: CVE-2026-26158
Upstream-Status: Backport [https://git.busybox.net/busybox/commit/?id=3fb6b31c716669e12f75a2accd31bb7685b1a1cb]
(Alternative mirrored URL: https://gogs.librecmc.org/OWEALS/busybox/commit/3fb6b31c716669e12f75a2accd31bb7685b1a1cb)
Signed-off-by: Ernst Persson <ernst.persson@non.se.com>
---
 archival/libarchive/data_extract_all.c      |  7 +++--
 archival/libarchive/get_header_tar.c        | 11 ++++++--
 archival/libarchive/unsafe_prefix.c         | 30 +++++++++++++++++----
 archival/libarchive/unsafe_symlink_target.c |  1 +
 archival/tar.c                              |  2 +-
 archival/unzip.c                            |  2 +-
 include/bb_archive.h                        |  3 ++-
 7 files changed, 42 insertions(+), 14 deletions(-)

diff --git a/archival/libarchive/data_extract_all.c b/archival/libarchive/data_extract_all.c
index 8a69711..b84b960 100644
--- a/archival/libarchive/data_extract_all.c
+++ b/archival/libarchive/data_extract_all.c
@@ -66,8 +66,8 @@ void FAST_FUNC data_extract_all(archive_handle_t *archive_handle)
 	}
 #endif
 #if ENABLE_FEATURE_PATH_TRAVERSAL_PROTECTION
-	/* Strip leading "/" and up to last "/../" path component */
-	dst_name = (char *)strip_unsafe_prefix(dst_name);
+	/* Skip leading "/" and past last ".." path component */
+	dst_name = (char *)skip_unsafe_prefix(dst_name);
 #endif
 // ^^^ This may be a problem if some applets do need to extract absolute names.
 // (Probably will need to invent ARCHIVE_ALLOW_UNSAFE_NAME flag).
@@ -185,8 +185,7 @@ void FAST_FUNC data_extract_all(archive_handle_t *archive_handle)
 
 		/* To avoid a directory traversal attack via symlinks,
 		 * do not restore symlinks with ".." components
-		 * or symlinks starting with "/", unless a magic
-		 * envvar is set.
+		 * or symlinks starting with "/"
 		 *
 		 * For example, consider a .tar created via:
 		 *  $ tar cvf bug.tar anything.txt
diff --git a/archival/libarchive/get_header_tar.c b/archival/libarchive/get_header_tar.c
index cc6f3f0..1c40ece 100644
--- a/archival/libarchive/get_header_tar.c
+++ b/archival/libarchive/get_header_tar.c
@@ -454,8 +454,15 @@ char FAST_FUNC get_header_tar(archive_handle_t *archive_handle)
 #endif
 
 	/* Everything up to and including last ".." component is stripped */
-	overlapping_strcpy(file_header->name, strip_unsafe_prefix(file_header->name));
-//TODO: do the same for file_header->link_target?
+	strip_unsafe_prefix(file_header->name);
+	if (file_header->link_target) {
+		/* GNU tar 1.34 examples:
+		 * tar: Removing leading '/' from hard link targets
+		 * tar: Removing leading '../' from hard link targets
+		 * tar: Removing leading 'etc/../' from hard link targets
+		 */
+		strip_unsafe_prefix(file_header->link_target);
+	}
 
 	/* Strip trailing '/' in directories */
 	/* Must be done after mode is set as '/' is used to check if it's a directory */
diff --git a/archival/libarchive/unsafe_prefix.c b/archival/libarchive/unsafe_prefix.c
index 6670811..89a371a 100644
--- a/archival/libarchive/unsafe_prefix.c
+++ b/archival/libarchive/unsafe_prefix.c
@@ -5,11 +5,11 @@
 #include "libbb.h"
 #include "bb_archive.h"
 
-const char* FAST_FUNC strip_unsafe_prefix(const char *str)
+const char* FAST_FUNC skip_unsafe_prefix(const char *str)
 {
 	const char *cp = str;
 	while (1) {
-		char *cp2;
+		const char *cp2;
 		if (*cp == '/') {
 			cp++;
 			continue;
@@ -22,10 +22,25 @@ const char* FAST_FUNC strip_unsafe_prefix(const char *str)
 			cp += 3;
 			continue;
 		}
-		cp2 = strstr(cp, "/../");
+		cp2 = cp;
+ find_dotdot:
+		cp2 = strstr(cp2, "/..");
 		if (!cp2)
-			break;
-		cp = cp2 + 4;
+			break; /* No (more) malicious components */
+
+		/* We found "/..something" */
+		cp2 += 3;
+		if (*cp2 != '/') {
+			if (*cp2 == '\0') {
+				/* Trailing "/..": malicious, return "" */
+				/* (causes harmless errors trying to create or hardlink a file named "") */
+				return cp2;
+			}
+			/* "/..name" is not malicious, look for next "/.." */
+			goto find_dotdot;
+		}
+		/* Found "/../": malicious, advance past it */
+		cp = cp2 + 1;
 	}
 	if (cp != str) {
 		static smallint warned = 0;
@@ -37,3 +52,8 @@ const char* FAST_FUNC strip_unsafe_prefix(const char *str)
 	}
 	return cp;
 }
+
+void FAST_FUNC strip_unsafe_prefix(char *str)
+{
+	overlapping_strcpy(str, skip_unsafe_prefix(str));
+}
diff --git a/archival/libarchive/unsafe_symlink_target.c b/archival/libarchive/unsafe_symlink_target.c
index f8dc803..d764c89 100644
--- a/archival/libarchive/unsafe_symlink_target.c
+++ b/archival/libarchive/unsafe_symlink_target.c
@@ -36,6 +36,7 @@ void FAST_FUNC create_links_from_list(llist_t *list)
 				*list->data ? "hard" : "sym",
 				list->data + 1, target
 			);
+			/* Note: GNU tar 1.34 errors out only _after_ all links are (attempted to be) created */
 		}
 		list = list->link;
 	}
diff --git a/archival/tar.c b/archival/tar.c
index d6ca6c1..d42dcfc 100644
--- a/archival/tar.c
+++ b/archival/tar.c
@@ -475,7 +475,7 @@ static int FAST_FUNC writeFileToTarball(struct recursive_state *state,
 	DBG("writeFileToTarball('%s')", fileName);
 
 	/* Strip leading '/' and such (must be before memorizing hardlink's name) */
-	header_name = strip_unsafe_prefix(fileName);
+	header_name = skip_unsafe_prefix(fileName);
 
 	if (header_name[0] == '\0')
 		return TRUE;
diff --git a/archival/unzip.c b/archival/unzip.c
index 71a3029..8a9a90f 100644
--- a/archival/unzip.c
+++ b/archival/unzip.c
@@ -860,7 +860,7 @@ int unzip_main(int argc, char **argv)
 
 		/* Guard against "/abspath", "/../" and similar attacks */
 // NB: UnZip 6.00 has option -: to disable this
-		overlapping_strcpy(dst_fn, strip_unsafe_prefix(dst_fn));
+		strip_unsafe_prefix(dst_fn);
 
 		/* Filter zip entries */
 		if (find_list_entry(zreject, dst_fn)
diff --git a/include/bb_archive.h b/include/bb_archive.h
index e0ef8fc..1dc77f3 100644
--- a/include/bb_archive.h
+++ b/include/bb_archive.h
@@ -202,7 +202,8 @@ char get_header_tar_xz(archive_handle_t *archive_handle) FAST_FUNC;
 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;
+const char *skip_unsafe_prefix(const char *str) FAST_FUNC;
+void strip_unsafe_prefix(char *str) FAST_FUNC;
 void create_or_remember_link(llist_t **link_placeholders,
 		const char *target,
 		const char *linkname,
