Commit fd54d99 for zlib
commit fd54d99ea36166fea3f5cf319c87b0aeb14b28c9
Author: Mark Adler <git@madler.net>
Date: Tue Nov 3 15:01:47 2026 -0800
Avoid path traversals in miniunz selectively by path components.
miniunz's path traversal protection would treat ".." as a problem
regardless of whether it was a path component or just in the
middle of a name. It would also remove leading periods whether or
not it was a traversal issue.
Now the path name is handled component by component, so only real
vulnerabilities are excised.
If the result is an empty name, it is now skipped with a message
instead of failing to open.
diff --git a/contrib/minizip/miniunz.c b/contrib/minizip/miniunz.c
index 33e9868..19724d1 100644
--- a/contrib/minizip/miniunz.c
+++ b/contrib/minizip/miniunz.c
@@ -184,33 +184,57 @@ static int makedir(const char *newdir) {
return 1;
}
-/* Return the part of name that can be created under the extraction directory:
- skip to the last "..", then past any leading separators, dots, or (on
- Windows) drive specifiers, so that the result is always a relative path. */
-static const char* safe_path(const char* name) {
- if (name[0]!='\0')
+/* Copy to out a version of the path name that can only be created under the
+ extraction directory. Both '/' and '\\' are taken as separators. Empty and
+ "." path components are removed, and a ".." component is removed along with
+ the component before it, if any. On Windows, drive specifiers at the start
+ and components of only dots and spaces are also removed. The result is
+ always a relative path, and is no longer than name. out must have room for
+ strlen(name) + 1 characters. */
+static void safe_path(char* out, const char* name) {
+ char* start = out;
+ while (*name != '\0')
{
- const char* relative_check = name;
- while (relative_check[1]!='\0')
- {
- if (relative_check[0]=='.' && relative_check[1]=='.')
- name = relative_check;
- relative_check++;
- }
- }
-
- for (;;)
- {
- if (name[0]=='/' || name[0]=='\\' || name[0]=='.')
+ const char* comp = name;
+ size_t len;
+ while (*name != '\0' && *name != '/' && *name != '\\')
name++;
+ len = (size_t)(name - comp);
#ifdef _WIN32
- else if (name[0]!='\0' && name[1]==':')
- name += 2;
+ while (out == start && len >= 2 && comp[1] == ':')
+ {
+ comp += 2; /* remove drive specifiers, e.g. "C:" */
+ len -= 2;
+ }
+ {
+ /* Windows drops trailing dots and spaces from a name, so remove
+ any other component made of only those, such as "..." */
+ size_t i = 0;
+ while (i < len && (comp[i] == '.' || comp[i] == ' '))
+ i++;
+ if (i == len && !(len == 2 && comp[0] == '.' && comp[1] == '.'))
+ len = 0;
+ }
#endif
- else
- break;
+ if (len == 2 && comp[0] == '.' && comp[1] == '.')
+ {
+ /* remove the previous component and its separator, if any */
+ if (out > start)
+ out--;
+ while (out > start && out[-1] != '/' && out[-1] != '\\')
+ out--;
+ }
+ else if (len != 0 && !(len == 1 && comp[0] == '.'))
+ {
+ memcpy(out, comp, len);
+ out += len;
+ if (*name != '\0')
+ *out++ = *name; /* keep the separator */
+ }
+ if (*name != '\0')
+ name++;
}
- return name;
+ *out = '\0';
}
static void do_banner(void) {
@@ -334,6 +358,7 @@ static int do_list(unzFile uf) {
static int do_extract_currentfile(unzFile uf, const int* popt_extract_without_path, int* popt_overwrite, const char* password) {
char filename_inzip[65536+1];
+ char write_name[65536+1];
char* filename_withoutpath;
char* p;
int err=UNZ_OK;
@@ -368,23 +393,27 @@ static int do_extract_currentfile(unzFile uf, const int* popt_extract_without_pa
if ((*filename_withoutpath)=='\0')
{
- const char* write_dirname = safe_path(filename_inzip);
- if (((*popt_extract_without_path)==0) && (write_dirname[0]!='\0'))
+ safe_path(write_name, filename_inzip);
+ if (((*popt_extract_without_path)==0) && (write_name[0]!='\0'))
{
- printf("creating directory: %s\n",write_dirname);
- mymkdir(write_dirname);
+ printf("creating directory: %s\n",write_name);
+ mymkdir(write_name);
}
}
else
{
- const char* write_filename;
+ const char* write_filename = write_name;
int skip=0;
if ((*popt_extract_without_path)==0)
- write_filename = filename_inzip;
+ safe_path(write_name, filename_inzip);
else
- write_filename = filename_withoutpath;
- write_filename = safe_path(write_filename);
+ safe_path(write_name, filename_withoutpath);
+ if (write_name[0]=='\0')
+ {
+ printf("skipping %s: no name left to extract to\n",filename_inzip);
+ skip = 1;
+ }
err = unzOpenCurrentFilePassword(uf,password);
if (err!=UNZ_OK)
@@ -429,14 +458,22 @@ static int do_extract_currentfile(unzFile uf, const int* popt_extract_without_pa
{
fout=FOPEN_FUNC(write_filename,"wb");
/* some zipfile don't contain directory alone before file */
- if ((fout==NULL) && ((*popt_extract_without_path)==0) &&
- (filename_withoutpath!=(char*)filename_inzip))
+ if ((fout==NULL) && ((*popt_extract_without_path)==0))
{
- char c=*(filename_withoutpath-1);
- *(filename_withoutpath-1)='\0';
- makedir(write_filename);
- *(filename_withoutpath-1)=c;
- fout=FOPEN_FUNC(write_filename,"wb");
+ /* make the directories leading up to the file */
+ char* sep = NULL;
+ char* q;
+ for (q = write_name; *q != '\0'; q++)
+ if (*q == '/' || *q == '\\')
+ sep = q;
+ if (sep != NULL)
+ {
+ char c = *sep;
+ *sep = '\0';
+ makedir(write_name);
+ *sep = c;
+ fout=FOPEN_FUNC(write_filename,"wb");
+ }
}
if (fout==NULL)