Hi,
Is there any progress with this please?
For now I think I was able to mitigate the issue via attached patch
file. It's using realpath() to check whether extracting file is in
expected root.
Thanks!
Petr
--- tar-1.35/src/common.h
+++ tar-1.35/src/common.h
@@ -724,6 +724,7 @@ size_t blocking_write (int fd, void cons
extern int chdir_current;
extern int chdir_fd;
+extern char *chdir_name;
int chdir_arg (char const *dir);
void chdir_do (int dir);
int chdir_count (void);
--- tar-1.35/src/extract.c
+++ tar-1.35/src/extract.c
@@ -58,6 +58,151 @@ #else
# define BIRTHTIME_EQ(a, b) true
#endif
+/*
+ * A wrapper around realpath(3) with optional use of resolvepath(2) when
+ * available (e.g., on Solaris or illumos). This provides a consistent way
+ * to canonicalize filesystem paths across platforms.
+ */
+
+#if defined(__sun) || defined(__SVR4)
+# define HAVE_RESOLVEPATH 1
+#endif
+
+char *portable_realpath(const char *path, char *resolved_path)
+{
+#if HAVE_RESOLVEPATH
+ if (path && path[0] == '/')
+ {
+ size_t bufsize = PATH_MAX;
+ char *buf = resolved_path ? resolved_path : malloc(bufsize);
+ if (!buf)
+ {
+ errno = ENOMEM;
+ return NULL;
+ }
+
+ ssize_t len = resolvepath(path, buf, bufsize);
+ if (len < 0)
+ {
+ int saved = errno;
+ if (!resolved_path) free(buf);
+ errno = saved;
+ return NULL;
+ }
+ if ((size_t)len >= bufsize)
+ {
+ if (!resolved_path) free(buf);
+ errno = ENAMETOOLONG;
+ return NULL;
+ }
+
+ buf[len] = '\0';
+ return buf;
+ }
+#endif
+ return realpath(path, resolved_path);
+}
+
+/* #define DEBUG 1 */
+
+/*
+ * Protect extraction against directory traversal (CVE-2025-45582 mitigation).
+ *
+ * Returns:
+ * 0 - OK, extraction allowed.
+ * 1 - Deny extraction (path is outside cached extraction root).
+ *
+ * Notes:
+ * - chdir_name is a global variable used by tar.
+ * - The extraction root is computed once and cached statically.
+ * - Must reset root_initialized = 0 whenever tar changes chdir_name
+ * or starts a new top-level extraction session.
+ */
+static int
+check_extract_within_root(const char *file_name)
+{
+ char resolved_extract_root[PATH_MAX];
+ char resolved_extract_dir[PATH_MAX];
+ char extract_dir[PATH_MAX];
+ char tmp_name[PATH_MAX];
+
+ /* ---------------------------
+ * Step 1: Get extraction root
+ * --------------------------- */
+ const char *base = chdir_name ? chdir_name : ".";
+
+ /* Canonicalize only the trusted base directory */
+ if (!portable_realpath(base, tmp_name))
+ return 0; /* test 93: -C x d -C y e (y/ is in x/) */
+
+ /* Build the intended extraction root literally,
+ without resolving possible symlinks in tar_dir. */
+ const char *slash;
+ int offset = 0;
+ if (file_name[0] == '.' && file_name[1] == '/') {
+ slash = strchr(file_name+2, '/'); /* file_name like "./dir/file" */
+ offset = 2;
+ } else {
+ slash = strchr(file_name, '/');
+ }
+ if (slash) {
+ size_t dirlen = slash - file_name - offset;
+ if (snprintf(resolved_extract_root, sizeof(resolved_extract_root),
+ "%s/%.*s", tmp_name, (int)dirlen, file_name + offset)
+ >= (int)sizeof(resolved_extract_root))
+ return 1;
+ } else {
+ /* Archive with flat files, no subdirs */
+ if (snprintf(resolved_extract_root, sizeof(resolved_extract_root),
+ "%s", tmp_name) >= (int)sizeof(resolved_extract_root))
+ return 1;
+ }
+
+#ifdef DEBUG
+ printf("DEBUG [init] resolved_extract_root=%s\n", resolved_extract_root);
+#endif
+
+ /* ------------------------------------
+ * Step 2: Canonicalize member's parent
+ * ------------------------------------ */
+ if (chdir_name) {
+ if (snprintf(extract_dir, sizeof(extract_dir), "%s/%s",
+ chdir_name, file_name) >= (int)sizeof(extract_dir))
+ return 1;
+ } else {
+ if (snprintf(extract_dir, sizeof(extract_dir), "%s", file_name)
+ >= (int)sizeof(extract_dir))
+ return 1;
+ }
+
+ char *last_slash = strrchr(extract_dir, '/');
+ if (last_slash)
+ *last_slash = '\0';
+ else
+ strcpy(extract_dir, ".");
+
+ if (!portable_realpath(extract_dir, resolved_extract_dir))
+ return 0; /* dir doesn't exist => 0 */
+
+ /* ---------------------------
+ * Step 3: Boundary check
+ * --------------------------- */
+ size_t root_len = strlen(resolved_extract_root);
+ int inside = 0;
+ if (strncmp(resolved_extract_root, resolved_extract_dir, root_len) == 0) {
+ char c = resolved_extract_dir[root_len];
+ if (c == '/' || c == '\0')
+ inside = 1;
+ }
+
+#ifdef DEBUG
+ printf("DEBUG [check] root=%s dir=%s inside=%d\n",
+ resolved_extract_root, resolved_extract_dir, inside);
+#endif
+
+ return inside ? 0 : 1;
+}
+
/* Return true if an error number ERR means the system call is
supported in this case. */
static bool
@@ -1273,6 +1418,13 @@ }
}
else
{
+ if (check_extract_within_root(file_name))
+ {
+ ERROR ((0, 0,
+ _("%s is out of extraction root"), file_name));
+ return 1;
+ }
+
int file_created;
/* Either we pre-create the file in set_xattr(), or we just directly open
the file in open_output_file() with O_CREAT. If pre-creating, we need
--- tar-1.35/src/misc.c
+++ tar-1.35/src/misc.c
@@ -968,6 +968,9 @@ descriptor, or AT_FDCWD if the working d
valid until the next invocation of chdir_do. */
int chdir_fd = AT_FDCWD;
+/* Name of -C dir or NULL */
+char *chdir_name = NULL;
+
/* Change to directory I, in a virtual way. This does not actually
invoke chdir; it merely sets chdir_fd to an int suitable as the
first argument for openat, etc. If I is 0, change to the initial
@@ -987,6 +990,7 @@ if (! IS_ABSOLUTE_FILE_NAME (curr->name)
chdir_do (i - 1);
fd = openat (chdir_fd, curr->name,
open_searchdir_flags & ~ O_NOFOLLOW);
+ chdir_name = strdup(curr->name);
if (fd < 0)
open_fatal (curr->name);