Sam James <s...@gentoo.org> writes:

> Collin Funk <collin.fu...@gmail.com> writes:
>
>> Paul Eggert <egg...@cs.ucla.edu> writes:
>>
>>> On 2025-08-07 08:47, Lior Kaplan wrote:
>>>> I was wondering if you had a chance to look at
>>>> https://nvd.nist.gov/vuln/detail/CVE-2025-45582
>>>
>>> First I've heard of it. Thanks for mentioning it.
>>>
>>> Sounds like tar by default should refuse to create symlinks to outside
>>> the working directory. Those symlinks are trouble anyway, regardless
>>> of whether the following program is tar or some other program.
>>
>> I agree with the behavior change.
>>
>> Extracting untrusted tar archives is already dangerous, so I don't think
>> it is worth any panic though.
>
> +1. Let's do this please.

I attached a patch which could probably be much improved. But works for
the original case.

Without '-P' we check for '..', so it seems reasonable to do it for
symlinks as well. Here is an example with this patch:

    $ rm -rf /home/ubuntu/.ssh
    $ ./src/tar -xf my_archive2.tar.gz 
    ./src/tar: Member component has link my_directory -> 
../../../../../../../home/ubuntu/ containing '..'
    ./src/tar: Member component has link my_directory -> 
../../../../../../../home/ubuntu/ containing '..'
    ./src/tar: Exiting with failure status due to previous errors
    $ stat /home/ubuntu/.ssh
    stat: cannot statx '/home/ubuntu/.ssh': No such file or directory

And using '-P' with this patch:

   $ ./src/tar -P -xf my_archive2.tar.gz
   $ stat --format=%n /home/ubuntu/.ssh/authorized_keys
   /home/ubuntu/.ssh/authorized_keys

I don't mind putting it behind a new option instead of '-P', but am not
creative enough to think of a long option name at the moment. :)

Collin

>From 9100a8768ee0b0925f9ca58356b3fbc3735a6c6e Mon Sep 17 00:00:00 2001
Message-ID: <9100a8768ee0b0925f9ca58356b3fbc3735a6c6e.1754637488.git.collin.fu...@gmail.com>
From: Collin Funk <collin.fu...@gmail.com>
Date: Thu, 7 Aug 2025 23:59:07 -0700
Subject: [PATCH] tar: disallow symlinks containing '..' unless -P is used

* src/extract.c: Include areadlink.h
(contains_dot_dot_safe): New function.
(extract_archive): Use it.
---
 src/extract.c | 66 +++++++++++++++++++++++++++++++++++++++++++++++----
 1 file changed, 62 insertions(+), 4 deletions(-)

diff --git a/src/extract.c b/src/extract.c
index 3b913c54..22d61680 100644
--- a/src/extract.c
+++ b/src/extract.c
@@ -27,6 +27,7 @@
 #include <priv-set.h>
 #include <root-uid.h>
 #include <utimens.h>
+#include <areadlink.h>
 
 #include "common.h"
 
@@ -1806,6 +1807,65 @@ prepare_to_extract (char const *file_name, char typeflag)
   return extractor;
 }
 
+/* Checks for '..' in FILE_NAME or any path components that are symlink
+   containing '..'.  */
+
+static bool
+contains_dot_dot_safe (char *file_name)
+{
+  /* Check for '..' without any symbolic links first.  */
+  if (contains_dot_dot (file_name))
+    {
+      paxerror (0, _("%s: Member name contains '..'"),
+                quotearg_colon (file_name));
+      return true;
+    }
+
+  char *p = file_name + FILE_SYSTEM_PREFIX_LEN (file_name);
+  char *end = p + strlen (p);
+
+  for (char *current = p; current < end; ++current)
+    {
+      char tmp;
+      struct stat st;
+      for (; ISSLASH (current[0]); ++current)
+        ;
+      for (; current < end && ! ISSLASH (current[0]); ++current)
+        ;
+      tmp = current[0];
+      current[0] = '\0';
+      if (fstatat (chdir_fd, file_name, &st, AT_SYMLINK_NOFOLLOW) < 0)
+        {
+          if (errno == ENOENT)
+            return false;
+          else
+            stat_error (file_name);
+        }
+      if (S_ISLNK (st.st_mode))
+        {
+          char *link_name = areadlinkat_with_size (chdir_fd, file_name,
+                                                   st.st_size);
+          if (link_name == nullptr)
+            readlink_error (file_name);
+          else
+            {
+              bool result = contains_dot_dot (link_name);
+              if (result)
+                {
+                  paxerror (0, _("Member component has link %s -> %s "
+                                 "containing '..'"),
+                            quotearg (file_name), quotearg_n (1, link_name));
+                  free (link_name);
+                  return true;
+                }
+              free (link_name);
+            }
+        }
+      current[0] = tmp;
+    }
+  return false;
+}
+
 /* Extract a file from the archive.  */
 void
 extract_archive (void)
@@ -1817,11 +1877,9 @@ extract_archive (void)
 
   set_next_block_after (current_header);
 
+
   skip_dotdot_name = (!absolute_names_option
-		      && contains_dot_dot (current_stat_info.orig_file_name));
-  if (skip_dotdot_name)
-    paxerror (0, _("%s: Member name contains '..'"),
-	      quotearg_colon (current_stat_info.orig_file_name));
+                      && contains_dot_dot_safe (current_stat_info.orig_file_name));
 
   if (!current_stat_info.file_name[0]
       || skip_dotdot_name
-- 
2.50.1

Reply via email to