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);
 

Reply via email to