Use openat2 to jailify the extraction directory

This addresses CVE-2025-45582.
* gnulib.modules: Add openat2.
* src/misc.c (open_subdir): New static function.
(fdbase_opendir): Use it.
* src/tar.c (open_searchdir_how): New var, replacing and
augmenting open_searchdir_flags.  All uses changed.
* tests/extrac31.at: New file.
* tests/Makefile (TESTSUITE_AT), tests/testuite.at: Add it.
This commit is contained in:
Paul Eggert
2025-11-13 13:44:10 -08:00
parent aec5d77437
commit 75b03fdff4
10 changed files with 107 additions and 29 deletions
+4 -1
View File
@@ -1,4 +1,4 @@
GNU tar NEWS - User visible changes. 2025-11-09 GNU tar NEWS - User visible changes. 2025-11-13
Please send GNU tar bug reports to <bug-tar@gnu.org> Please send GNU tar bug reports to <bug-tar@gnu.org>
version 1.35.90 (git) version 1.35.90 (git)
@@ -35,6 +35,9 @@ option.
* Bug fixes * Bug fixes
** When extracting, tar no longer follows symbolic links to targets
outside the working directory.
** Fixed O(n^2) time complexity bug for large numbers of directories when ** Fixed O(n^2) time complexity bug for large numbers of directories when
extracting with --delay-directory-restore or reading incremental archives. extracting with --delay-directory-restore or reading incremental archives.
+21 -18
View File
@@ -13128,26 +13128,31 @@ when you later extract from the archive you will get incorrect data.
When @command{tar} extracts from an archive, by default it writes into When @command{tar} extracts from an archive, by default it writes into
files relative to the working directory. If the archive was generated files relative to the working directory. If the archive was generated
by an untrusted user, that user therefore can write into any file by an untrusted user, that user therefore can write into any file
under the working directory. If the working directory contains a under the working directory. When extracting from an
symbolic link to another directory, the untrusted user can also write
into any file under the referenced directory. When extracting from an
untrusted archive, it is therefore good practice to create an empty untrusted archive, it is therefore good practice to create an empty
directory and run @command{tar} in that directory. directory and run @command{tar} in that directory. You can use the
@option{--directory} (@option{-C}) option to specify the working
directory (@pxref{directory}).
When extracting from two or more untrusted archives, each one should When extracting from an archive, @command{tar} rejects attempts to
be extracted independently, into different empty directories. modify files outside the working directory.
Otherwise, the first archive could create a symbolic link into an area For example, if a symbolic link points outside the working directory,
outside the working directory, and the second one could follow the @command{tar} refuses to follow the link, regardless of whether the
link and overwrite data that is not under the working directory. For symbolic link existed before @command{tar} was run.
example, when restoring from a series of incremental dumps, the Therefore, when extracting from two or more untrusted archives,
archives should have been created by a trusted process, as otherwise each one can be extracted in turn, into the same initially-empty directory.
the incremental restores might alter data outside the working Even if an earlier archive creates a symbolic link that
directory. points outside the working directory,
@command{tar} will reject any later attempts to follow that symbolic link.
However, this safety mechanism applies only to @command{tar} itself:
it does not apply to other programs you may run later, which will
ordinarily follow symbolic links even if they escape the working directory.
If you use the @option{--absolute-names} (@option{-P}) option when If you use the @option{--absolute-names} (@option{-P}) option when
extracting, @command{tar} respects any file names in the archive, even extracting, @command{tar} respects any file names in the archive, even
file names that begin with @file{/} or contain @file{..}. As this file names that begin with @file{/}, contain @file{..}, or that follow
lets the archive overwrite any file in your system that you can write, a symbolic link to escape the extraction directory. As this lets the
archive overwrite any file in your system that you can write,
the @option{--absolute-names} (@option{-P}) option should be used only the @option{--absolute-names} (@option{-P}) option should be used only
for trusted archives. for trusted archives.
@@ -13217,7 +13222,7 @@ Protect archives at least as much as you protect any of the files
being archived. being archived.
@item @item
Extract from an untrusted archive only into an otherwise-empty Extract from untrusted archives only into an otherwise-empty
directory. This directory and its parent should be accessible only to directory. This directory and its parent should be accessible only to
trusted users. For example: trusted users. For example:
@@ -13230,8 +13235,6 @@ $ @kbd{tar -xvf /archives/got-it-off-the-net.tar.gz}
@end group @end group
@end example @end example
As a corollary, do not do an incremental restore from an untrusted archive.
@item @item
Do not let untrusted users access files extracted from untrusted Do not let untrusted users access files extracted from untrusted
archives without checking first for problems such as setuid programs. archives without checking first for problems such as setuid programs.
+1
View File
@@ -84,6 +84,7 @@ mkfifoat
modechange modechange
obstack obstack
openat openat
openat2
parse-datetime parse-datetime
priv-set priv-set
progname progname
+1 -1
View File
@@ -376,7 +376,7 @@ struct name
/* Flags for reading, searching, and fstatatting files. */ /* Flags for reading, searching, and fstatatting files. */
extern int open_read_flags; extern int open_read_flags;
extern int open_searchdir_flags; extern struct open_how open_searchdir_how;
extern int fstatat_flags; extern int fstatat_flags;
extern int seek_option; extern int seek_option;
+3 -3
View File
@@ -1344,7 +1344,7 @@ create_archive (void)
struct fdbase f = fdbase (p->name); struct fdbase f = fdbase (p->name);
int fd = (f.fd == BADFD ? -1 int fd = (f.fd == BADFD ? -1
: openat (f.fd, f.base, : openat (f.fd, f.base,
open_searchdir_flags)); open_searchdir_how.flags));
if (fd < 0) if (fd < 0)
{ {
file_removed_diag (p->name, !p->parent, file_removed_diag (p->name, !p->parent,
@@ -1569,7 +1569,7 @@ restore_parent_fd (struct tar_stat_info const *st)
struct tar_stat_info *parent = st->parent; struct tar_stat_info *parent = st->parent;
if (parent && ! parent->fd) if (parent && ! parent->fd)
{ {
int parentfd = openat (st->fd, "..", open_searchdir_flags); int parentfd = openat (st->fd, "..", open_searchdir_how.flags);
struct stat parentstat; struct stat parentstat;
if (parentfd < 0) if (parentfd < 0)
@@ -1585,7 +1585,7 @@ restore_parent_fd (struct tar_stat_info const *st)
{ {
struct fdbase f = fdbase (parent->orig_file_name); struct fdbase f = fdbase (parent->orig_file_name);
int origfd = (f.fd == BADFD ? -1 int origfd = (f.fd == BADFD ? -1
: openat (f.fd, f.base, open_searchdir_flags)); : openat (f.fd, f.base, open_searchdir_how.flags));
if (0 <= origfd) if (0 <= origfd)
{ {
if (fstat (parentfd, &parentstat) < 0 if (fstat (parentfd, &parentstat) < 0
+13 -3
View File
@@ -1070,7 +1070,7 @@ chdir_do (idx_t i)
if (! IS_ABSOLUTE_FILE_NAME (curr->name)) if (! IS_ABSOLUTE_FILE_NAME (curr->name))
chdir_do (i - 1); chdir_do (i - 1);
fd = openat (chdir_fd, curr->name, fd = openat (chdir_fd, curr->name,
open_searchdir_flags & ~ O_NOFOLLOW); open_searchdir_how.flags & ~O_NOFOLLOW);
if (fd < 0) if (fd < 0)
open_fatal (curr->name); open_fatal (curr->name);
@@ -1173,6 +1173,16 @@ fdbase_clear (void)
} }
} }
/* Starting from the directory FD, open a subdirectory SUBDIR for search.
If extracting or diffing and --absolute-names (-P) is not in effect,
do not let the subdirectory escape FD, i.e., the subdirectory must
be at or under FD in the directory hierarchy. */
static int
open_subdir (int fd, char const *subdir)
{
return openat2 (fd, subdir, &open_searchdir_how, sizeof open_searchdir_how);
}
/* Return an fd open to FILE_NAME's parent directory, /* Return an fd open to FILE_NAME's parent directory,
along with the base name of FILE_NAME. along with the base name of FILE_NAME.
Use the alternate cache if ALTERNATE, the main cache otherwise. Use the alternate cache if ALTERNATE, the main cache otherwise.
@@ -1224,7 +1234,7 @@ fdbase_opendir (char const *file_name, bool alternate)
{ {
/* The new directory is a subdirectory of the old, /* The new directory is a subdirectory of the old,
so open relative to FD rather than to chdir_fd. */ so open relative to FD rather than to chdir_fd. */
int subfd = openat (fd, &subdir[c->subdirlen], open_searchdir_flags); int subfd = open_subdir (fd, &subdir[c->subdirlen]);
if (subfd < 0) if (subfd < 0)
{ {
/* Keep the old directory cached and report open failure, /* Keep the old directory cached and report open failure,
@@ -1251,7 +1261,7 @@ fdbase_opendir (char const *file_name, bool alternate)
and add new info if the new directory can be opened. */ and add new info if the new directory can be opened. */
if (0 < c->subdirlen) if (0 < c->subdirlen)
close (fd); close (fd);
fd = openat (chdir_fd, c->subdir, open_searchdir_flags); fd = open_subdir (chdir_fd, c->subdir);
if (fd < 0) if (fd < 0)
{ {
if (BADFD != -1 && fd < 0) if (BADFD != -1 && fd < 0)
+7 -3
View File
@@ -111,7 +111,7 @@ idx_t archive_names;
const char **archive_name_cursor; const char **archive_name_cursor;
char const *index_file_name; char const *index_file_name;
int open_read_flags; int open_read_flags;
int open_searchdir_flags; struct open_how open_searchdir_how;
int fstatat_flags; int fstatat_flags;
int seek_option; int seek_option;
bool unquote_option; bool unquote_option;
@@ -2709,8 +2709,12 @@ decode_options (int argc, char **argv)
#else #else
int search_flags = O_SEARCH | noatime_flag; int search_flags = O_SEARCH | noatime_flag;
#endif #endif
open_searchdir_flags = (search_flags | O_BINARY | O_CLOEXEC | O_DIRECTORY open_searchdir_how.flags = (search_flags | nofollow_flag
| nofollow_flag); | O_BINARY | O_CLOEXEC | O_DIRECTORY);
if (!absolute_names_option
&& (subcommand_option == EXTRACT_SUBCOMMAND
|| subcommand_option == DIFF_SUBCOMMAND))
open_searchdir_how.resolve = RESOLVE_BENEATH;
} }
fstatat_flags = dereference_option ? 0 : AT_SYMLINK_NOFOLLOW; fstatat_flags = dereference_option ? 0 : AT_SYMLINK_NOFOLLOW;
+1
View File
@@ -140,6 +140,7 @@ TESTSUITE_AT = \
extrac28.at\ extrac28.at\
extrac29.at\ extrac29.at\
extrac30.at\ extrac30.at\
extrac31.at\
filerem01.at\ filerem01.at\
filerem02.at\ filerem02.at\
grow.at\ grow.at\
+55
View File
@@ -0,0 +1,55 @@
# Test suite for GNU tar. -*- Autotest -*-
# Copyright 2025 Free Software Foundation, Inc.
#
# This file is part of GNU tar.
#
# GNU tar is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# GNU tar is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
AT_SETUP([extracting untrusted incremental])
AT_KEYWORDS([extract extrac31 --absolute-names])
AT_TAR_CHECK([
# Extraction should not escape the extraction directory
# even when extracting multiple times to the same directory.
(umask 022 && mkdir -p dira/sub dirb/sym dirb/sub/sym ext victimdir victimexp)
ln -s .. dira/sub/dotdot
ln -s ../sub dira/sub/dot
ln -s dotdot/sub dira/sub/anotherdot
ln -s ../victimdir dira/sym
ln -s dotdot/../victimdir dira/sub/sym
echo b1 >dirb/sym/file1
echo b2 >dirb/sub/sym/file2
echo v >victimdir/expected
echo v >victimdir/file1
echo v >victimdir/file2
cp victimdir/* victimexp
tar -cf a.tar -C dira sub sym
tar -cf b.tar -C dirb sym/file1 sub/sym/file2
tar -xf a.tar -C ext
echo status1=$?
tar -xf b.tar -C ext
echo status2=$?
diff victimdir victimexp
],
[],
[status1=0
status2=2
],
[tar: sym/file1: Cannot open: Invalid cross-device link
tar: sub/sym/file2: Cannot open: Invalid cross-device link
tar: Exiting with failure status due to previous errors
])
AT_CLEANUP
+1
View File
@@ -357,6 +357,7 @@ m4_include([extrac27.at])
m4_include([extrac28.at]) m4_include([extrac28.at])
m4_include([extrac29.at]) m4_include([extrac29.at])
m4_include([extrac30.at]) m4_include([extrac30.at])
m4_include([extrac31.at])
m4_include([backup01.at]) m4_include([backup01.at])