This adds the --one-top-level option to reject tarballs that do not follow the practice of having everything in one directory. The only option that appears to be in conflict is --absolute-names because the code assumes that unsafe prefixes like ".." and leading slashes are removed. Archives that fail the check cause an error that aborts extraction. This leaves it up to the user to apply an appropriate workaround.
Signed-off-by: Connor Behan <connor.be...@gmail.com> --- NEWS | 8 ++++++++ doc/tar.texi | 9 +++++++++ src/common.h | 3 +++ src/extract.c | 43 +++++++++++++++++++++++++++++++++++++++++++ src/tar.c | 10 ++++++++++ 5 files changed, 73 insertions(+) diff --git a/NEWS b/NEWS index 3108798..0abc2e5 100644 --- a/NEWS +++ b/NEWS @@ -39,6 +39,14 @@ errors. Instead it just silently skips them. An additional level of verbosity can be obtained by using the option --warning=existing-file together with this option. +* The --one-top-level option. + +This new command line option tells tar that the working directory +(or the one passed to -C) should not be populated with more than one +name directly under it. A fatal error is thrown if, after name +transformations, a second member that would be extracted to this location +is found. + * Support for POSIX ACLs, extended attributes and SELinux context. Starting with this version tar is able to store, extract and list diff --git a/doc/tar.texi b/doc/tar.texi index ddfa055..b3b1e77 100644 --- a/doc/tar.texi +++ b/doc/tar.texi @@ -3071,6 +3071,15 @@ Used when creating an archive. Prevents @command{tar} from recursing into directories that are on different file systems from the current directory. +@opsummary{one-top-level} +@item --one-top-level + +Tells @command{tar} that only one name from the archive should be placed directly +under the extraction directory (the working directory or the one specified with +@option{-C}). A fatal error is thrown if a second one is found. The top-level of +a member is determined after the transformations from @option{--strip-components} +and @option{--transform} are applied. + @opsummary{overwrite} @item --overwrite diff --git a/src/common.h b/src/common.h index 723ad90..8a5e6f3 100644 --- a/src/common.h +++ b/src/common.h @@ -233,6 +233,9 @@ GLOBAL bool numeric_owner_option; GLOBAL bool one_file_system_option; +/* Refuse to extract more than one top-level name. */ +GLOBAL bool one_top_level_option; + /* Specified value to be put into tar file in place of stat () results, or just null and -1 if such an override should not take place. */ GLOBAL char const *owner_name_option; diff --git a/src/extract.c b/src/extract.c index 3d8ba10..0292f0c 100644 --- a/src/extract.c +++ b/src/extract.c @@ -1559,6 +1559,45 @@ prepare_to_extract (char const *file_name, int typeflag, tar_extractor_t *fun) return 1; } +/* Throws an error if passed file name is the second top-level name. */ +void +depth_check (char *file_name) +{ + size_t i; + size_t start = 0; + + static char *first_top_level; + static bool first_top_level_found = false; + + /* Leading slashes have already been removed so it is safe to start at 1. */ + for (i = 1; i <= strlen (file_name); i++) + { + if (ISSLASH (file_name[i]) || file_name[i] == '\0') + { + if ((i == start) || (i - start == 1 && file_name[start] == '.')) + start = i + 1; + else + { + if (first_top_level_found) + { + /* Still reject two top-levels if one is a substring of the other. */ + if (strncmp (first_top_level, file_name + start, i - start) + || strlen (first_top_level) != i - start) + FATAL_ERROR ((0, 0, _("Found multiple top-level names, exiting"))); + } + else + { + first_top_level_found = true; + first_top_level = xmalloc (i - start + 1); + strncpy (first_top_level, file_name + start, i - start); + first_top_level[i - start] = '\0'; + } + return; + } + } + } +} + /* Extract a file from the archive. */ void extract_archive (void) @@ -1604,6 +1643,10 @@ extract_archive (void) return; } + /* Check for names where the depth is too high. */ + if (one_top_level_option) + depth_check (current_stat_info.file_name); + /* Extract the archive entry according to its type. */ /* KLUDGE */ typeflag = sparse_member_p (¤t_stat_info) ? diff --git a/src/tar.c b/src/tar.c index 6dc2ec5..10ff4c8 100644 --- a/src/tar.c +++ b/src/tar.c @@ -318,6 +318,7 @@ enum OCCURRENCE_OPTION, OLD_ARCHIVE_OPTION, ONE_FILE_SYSTEM_OPTION, + ONE_TOP_LEVEL_OPTION, OVERWRITE_DIR_OPTION, OVERWRITE_OPTION, OWNER_OPTION, @@ -485,6 +486,8 @@ static struct argp_option options[] = { {"overwrite-dir", OVERWRITE_DIR_OPTION, 0, 0, N_("overwrite metadata of existing directories when extracting (default)"), GRID+1 }, + {"one-top-level", ONE_TOP_LEVEL_OPTION, 0, 0, + N_("allow at most one top-level name when extracting"), GRID+1 }, #undef GRID #define GRID 40 @@ -1433,6 +1436,10 @@ parse_opt (int key, char *arg, struct argp_state *state) one_file_system_option = true; break; + case ONE_TOP_LEVEL_OPTION: + one_top_level_option = true; + break; + case 'l': check_links_option = 1; break; @@ -2381,6 +2388,9 @@ decode_options (int argc, char **argv) subcommand_string (subcommand_option))); } + if (one_top_level_option && absolute_names_option) + USAGE_ERROR ((0, 0, _("--one-top-level cannot be used with --absolute-names"))); + if (archive_names == 0) { /* If no archive file name given, try TAPE from the environment, or -- 1.8.4