From 3fb6b31c716669e12f75a2accd31bb7685b1a1cb Mon Sep 17 00:00:00 2001 From: Denys Vlasenko 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 CVE: CVE-2026-26157 CVE: CVE-2026-26158 Upstream: https://git.busybox.net/busybox/commit/?id=3fb6b31c716669e12f75a2accd31bb7685b1a1cb Upstream: https://gogs.librecmc.org/OWEALS/busybox/commit/3fb6b31c716669e12f75a2accd31bb7685b1a1cb Signed-off-by: Hitendra Prajapati Signed-off-by: Thomas Perale --- 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 9de3759..cf8c2d1 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 691a2d8..5844215 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, -- 2.50.1