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