CVE-2021-40153: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal') (CWE-22)
value: squashfs_opendir in unsquash-1.c in Squashfs-Tools 4.5 stores the filename in the directory entry; this is then used by unsquashfs to create the new file during the unsquash. The filename is not validated for traversal outside of the destination directory, and thus allows writing to locations outside of the destination.
CWE description: The product uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the product does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory.
CVSS v3 base score: 8.1
Published: 2021-08-27T15:15Z
Repository: https://github.com/plougher/squashfs-tools
Language: C
Files changed: squashfs-tools/Makefile, squashfs-tools/unsquash-1.c, squashfs-tools/unsquash-1234.c, squashfs-tools/unsquash-2.c, squashfs-tools/unsquash-3.c, squashfs-tools/unsquash-4.c, squashfs-tools/unsquashfs.c, squashfs-tools/unsquashfs.h
Commit date: 2021-01-17 18:49:37 +0000
Commit message: Unsquashfs: fix write outside destination directory exploit

An issue on Github (https://github.com/plougher/squashfs-tools/issues/72)
shows how some specially crafted Squashfs filesystems containing
invalid file names (with '/' and ..) can cause Unsquashfs to write
files outside of the destination directory.

This commit fixes this exploit by checking all names for
validity.

In doing so I have also added checks for '.' and for names that
are shorter than they should be (names in the file system should
not have '\0' terminators).

Signed-off-by: Phillip Lougher <phillip@squashfs.org.uk>

Diff: diff --git a/squashfs-tools/Makefile b/squashfs-tools/Makefile
index aee4b960..20feaca2 100644
--- a/squashfs-tools/Makefile
+++ b/squashfs-tools/Makefile
@@ -154,11 +154,12 @@ INSTALL_DIR = /usr/local/bin
 MKSQUASHFS_OBJS = mksquashfs.o read_fs.o action.o swap.o pseudo.o compressor.o \
 	sort.o progressbar.o read_file.o info.o restore.o process_fragments.o \
 	caches-queues-lists.o
 
 UNSQUASHFS_OBJS = unsquashfs.o unsquash-1.o unsquash-2.o unsquash-3.o \
-	unsquash-4.o unsquash-123.o unsquash-34.o swap.o compressor.o unsquashfs_info.o
+	unsquash-4.o unsquash-123.o unsquash-34.o unsquash-1234.o swap.o \
+	compressor.o unsquashfs_info.o
 
 CFLAGS ?= -O2
 CFLAGS += $(EXTRA_CFLAGS) $(INCLUDEDIR) -D_FILE_OFFSET_BITS=64 \
 	-D_LARGEFILE_SOURCE -D_GNU_SOURCE -DCOMP_DEFAULT=\"$(COMP_DEFAULT)\" \
 	-Wall
@@ -348,10 +349,12 @@ unsquash-4.o: unsquashfs.h unsquash-4.c squashfs_fs.h squashfs_swap.h \
 
 unsquash-123.o: unsquashfs.h unsquash-123.c squashfs_fs.h squashfs_compat.h
 
 unsquash-34.o: unsquashfs.h unsquash-34.c
 
+unsquash-1234.o: unsquash-1234.c
+
 unsquashfs_xattr.o: unsquashfs_xattr.c unsquashfs.h squashfs_fs.h xattr.h
 
 unsquashfs_info.o: unsquashfs.h squashfs_fs.h
 
 .PHONY: clean
diff --git a/squashfs-tools/unsquash-1.c b/squashfs-tools/unsquash-1.c
index 34eced36..28326cb1 100644
--- a/squashfs-tools/unsquash-1.c
+++ b/squashfs-tools/unsquash-1.c
@@ -1,10 +1,10 @@
 /*
  * Unsquash a squashfs filesystem.  This is a highly compressed read only
  * filesystem.
  *
- * Copyright (c) 2009, 2010, 2011, 2012, 2019
+ * Copyright (c) 2009, 2010, 2011, 2012, 2019, 2021
  * Phillip Lougher <phillip@squashfs.org.uk>
  *
  * This program 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 2,
@@ -283,10 +283,17 @@ static struct dir *squashfs_opendir(unsigned int block_start, unsigned int offse
 			}
 
 			memcpy(dire->name, directory_table + bytes,
 				dire->size + 1);
 			dire->name[dire->size + 1] = '\0';
+
+			/* check name for invalid characters (i.e /, ., ..) */
+			if(check_name(dire->name, dire->size + 1) == FALSE) {
+				ERROR("File system corrupted: invalid characters in name\n");
+				goto corrupted;
+			}
+
 			TRACE("squashfs_opendir: directory entry %s, inode "
 				"%d:%d, type %d\n", dire->name,
 				dirh.start_block, dire->offset, dire->type);
 			if((dir->dir_count % DIR_ENT_SIZE) == 0) {
 				new_dir = realloc(dir->dirs, (dir->dir_count +
diff --git a/squashfs-tools/unsquash-1234.c b/squashfs-tools/unsquash-1234.c
new file mode 100644
index 00000000..c2d4f42b
--- /dev/null
+++ b/squashfs-tools/unsquash-1234.c
@@ -0,0 +1,58 @@
+/*
+ * Unsquash a squashfs filesystem.  This is a highly compressed read only
+ * filesystem.
+ *
+ * Copyright (c) 2021
+ * Phillip Lougher <phillip@squashfs.org.uk>
+ *
+ * This program 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 2,
+ * or (at your option) any later version.
+ *
+ * This program 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, write to the Free Software
+ * Foundation, 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
+ * unsquash-1234.c
+ *
+ * Helper functions used by unsquash-1, unsquash-2, unsquash-3 and
+ * unsquash-4.
+ */
+
+#define TRUE 1
+#define FALSE 0
+/*
+ * Check name for validity, name should not
+ *  - be ".", "./", or
+ *  - be "..", "../" or
+ *  - have a "/" anywhere in the name, or
+ *  - be shorter than the expected size
+ */
+int check_name(char *name, int size)
+{
+	char *start = name;
+
+	if(name[0] == '.') {
+		if(name[1] == '.')
+			name++;
+		if(name[1] == '/' || name[1] == '\0')
+			return FALSE;
+	}
+
+	while(name[0] != '/' && name[0] != '\0')
+		name ++;
+
+	if(name[0] == '/')
+		return FALSE;
+
+	if((name - start) != size)
+		return FALSE;
+
+	return TRUE;
+}
diff --git a/squashfs-tools/unsquash-2.c b/squashfs-tools/unsquash-2.c
index 4b3d767e..474064e1 100644
--- a/squashfs-tools/unsquash-2.c
+++ b/squashfs-tools/unsquash-2.c
@@ -1,10 +1,10 @@
 /*
  * Unsquash a squashfs filesystem.  This is a highly compressed read only
  * filesystem.
  *
- * Copyright (c) 2009, 2010, 2013, 2019
+ * Copyright (c) 2009, 2010, 2013, 2019, 2021
  * Phillip Lougher <phillip@squashfs.org.uk>
  *
  * This program 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 2,
@@ -384,10 +384,17 @@ static struct dir *squashfs_opendir(unsigned int block_start, unsigned int offse
 			}
 
 			memcpy(dire->name, directory_table + bytes,
 				dire->size + 1);
 			dire->name[dire->size + 1] = '\0';
+
+			/* check name for invalid characters (i.e /, ., ..) */
+			if(check_name(dire->name, dire->size + 1) == FALSE) {
+				ERROR("File system corrupted: invalid characters in name\n");
+				goto corrupted;
+			}
+
 			TRACE("squashfs_opendir: directory entry %s, inode "
 				"%d:%d, type %d\n", dire->name,
 				dirh.start_block, dire->offset, dire->type);
 			if((dir->dir_count % DIR_ENT_SIZE) == 0) {
 				new_dir = realloc(dir->dirs, (dir->dir_count +
diff --git a/squashfs-tools/unsquash-3.c b/squashfs-tools/unsquash-3.c
index 02c31fc5..65cfe4d9 100644
--- a/squashfs-tools/unsquash-3.c
+++ b/squashfs-tools/unsquash-3.c
@@ -1,10 +1,10 @@
 /*
  * Unsquash a squashfs filesystem.  This is a highly compressed read only
  * filesystem.
  *
- * Copyright (c) 2009, 2010, 2011, 2012, 2013, 2019
+ * Copyright (c) 2009, 2010, 2011, 2012, 2013, 2019, 2021
  * Phillip Lougher <phillip@squashfs.org.uk>
  *
  * This program 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 2,
@@ -411,10 +411,17 @@ static struct dir *squashfs_opendir(unsigned int block_start, unsigned int offse
 			}
 
 			memcpy(dire->name, directory_table + bytes,
 				dire->size + 1);
 			dire->name[dire->size + 1] = '\0';
+
+			/* check name for invalid characters (i.e /, ., ..) */
+			if(check_name(dire->name, dire->size + 1) == FALSE) {
+				ERROR("File system corrupted: invalid characters in name\n");
+				goto corrupted;
+			}
+
 			TRACE("squashfs_opendir: directory entry %s, inode "
 				"%d:%d, type %d\n", dire->name,
 				dirh.start_block, dire->offset, dire->type);
 			if((dir->dir_count % DIR_ENT_SIZE) == 0) {
 				new_dir = realloc(dir->dirs, (dir->dir_count +
diff --git a/squashfs-tools/unsquash-4.c b/squashfs-tools/unsquash-4.c
index 8475835c..aa23a841 100644
--- a/squashfs-tools/unsquash-4.c
+++ b/squashfs-tools/unsquash-4.c
@@ -1,10 +1,10 @@
 /*
  * Unsquash a squashfs filesystem.  This is a highly compressed read only
  * filesystem.
  *
- * Copyright (c) 2009, 2010, 2011, 2012, 2013, 2019
+ * Copyright (c) 2009, 2010, 2011, 2012, 2013, 2019, 2021
  * Phillip Lougher <phillip@squashfs.org.uk>
  *
  * This program 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 2,
@@ -347,10 +347,17 @@ static struct dir *squashfs_opendir(unsigned int block_start, unsigned int offse
 			}
 
 			memcpy(dire->name, directory_table + bytes,
 				dire->size + 1);
 			dire->name[dire->size + 1] = '\0';
+
+			/* check name for invalid characters (i.e /, ., ..) */
+			if(check_name(dire->name, dire->size + 1) == FALSE) {
+				ERROR("File system corrupted: invalid characters in name\n");
+				goto corrupted;
+			}
+
 			TRACE("squashfs_opendir: directory entry %s, inode "
 				"%d:%d, type %d\n", dire->name,
 				dirh.start_block, dire->offset, dire->type);
 			if((dir->dir_count % DIR_ENT_SIZE) == 0) {
 				new_dir = realloc(dir->dirs, (dir->dir_count +
diff --git a/squashfs-tools/unsquashfs.c b/squashfs-tools/unsquashfs.c
index ffd965e5..8a84571d 100644
--- a/squashfs-tools/unsquashfs.c
+++ b/squashfs-tools/unsquashfs.c
@@ -2668,11 +2668,11 @@ int parse_number(char *start, int *res)
 	return 1;
 }
 
 
 #define VERSION() \
-	printf("unsquashfs version 4.4-git (2021/01/09)\n");\
+	printf("unsquashfs version 4.4-git (2021/01/17)\n");\
 	printf("copyright (C) 2021 Phillip Lougher "\
 		"<phillip@squashfs.org.uk>\n\n");\
     	printf("This program is free software; you can redistribute it and/or"\
 		"\n");\
 	printf("modify it under the terms of the GNU General Public License"\
diff --git a/squashfs-tools/unsquashfs.h b/squashfs-tools/unsquashfs.h
index 934618b2..db1da7a0 100644
--- a/squashfs-tools/unsquashfs.h
+++ b/squashfs-tools/unsquashfs.h
@@ -2,11 +2,11 @@
 #define UNSQUASHFS_H
 /*
  * Unsquash a squashfs filesystem.  This is a highly compressed read only
  * filesystem.
  *
- * Copyright (c) 2009, 2010, 2013, 2014, 2019
+ * Copyright (c) 2009, 2010, 2013, 2014, 2019, 2021
  * Phillip Lougher <phillip@squashfs.org.uk>
  *
  * This program 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 2,
@@ -259,6 +259,9 @@ extern squashfs_operations *read_filesystem_tables_4();
 /* unsquash-123.c */
 extern int read_ids(int, long long, long long, unsigned int **);
 
 /* unsquash-34.c */
 extern long long *alloc_index_table(int);
+
+/* unsquash-1234.c */
+extern int check_name(char *, int);
 #endif

Vulnerable code: 	unsquash-4.o unsquash-123.o unsquash-34.o swap.o compressor.o unsquashfs_info.o
 * Copyright (c) 2009, 2010, 2011, 2012, 2019
 * Copyright (c) 2009, 2010, 2013, 2019
 * Copyright (c) 2009, 2010, 2011, 2012, 2013, 2019
 * Copyright (c) 2009, 2010, 2011, 2012, 2013, 2019
	printf("unsquashfs version 4.4-git (2021/01/09)\n");\
 * Copyright (c) 2009, 2010, 2013, 2014, 2019
Fixed code: 	unsquash-4.o unsquash-123.o unsquash-34.o unsquash-1234.o swap.o \
	compressor.o unsquashfs_info.o
unsquash-1234.o: unsquash-1234.c

 * Copyright (c) 2009, 2010, 2011, 2012, 2019, 2021

			/* check name for invalid characters (i.e /, ., ..) */
			if(check_name(dire->name, dire->size + 1) == FALSE) {
				ERROR("File system corrupted: invalid characters in name\n");
				goto corrupted;
			}

/*
 * Unsquash a squashfs filesystem.  This is a highly compressed read only
 * filesystem.
 *
 * Copyright (c) 2021
 * Phillip Lougher <phillip@squashfs.org.uk>
 *
 * This program 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 2,
 * or (at your option) any later version.
 *
 * This program 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, write to the Free Software
 * Foundation, 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 *
 * unsquash-1234.c
 *
 * Helper functions used by unsquash-1, unsquash-2, unsquash-3 and
 * unsquash-4.
 */

#define TRUE 1
#define FALSE 0
/*
 * Check name for validity, name should not
 *  - be ".", "./", or
 *  - be "..", "../" or
 *  - have a "/" anywhere in the name, or
 *  - be shorter than the expected size
 */
int check_name(char *name, int size)
{
	char *start = name;

	if(name[0] == '.') {
		if(name[1] == '.')
			name++;
		if(name[1] == '/' || name[1] == '\0')
			return FALSE;
	}

	while(name[0] != '/' && name[0] != '\0')
		name ++;

	if(name[0] == '/')
		return FALSE;

	if((name - start) != size)
		return FALSE;

	return TRUE;
}
 * Copyright (c) 2009, 2010, 2013, 2019, 2021

			/* check name for invalid characters (i.e /, ., ..) */
			if(check_name(dire->name, dire->size + 1) == FALSE) {
				ERROR("File system corrupted: invalid characters in name\n");
				goto corrupted;
			}

 * Copyright (c) 2009, 2010, 2011, 2012, 2013, 2019, 2021

			/* check name for invalid characters (i.e /, ., ..) */
			if(check_name(dire->name, dire->size + 1) == FALSE) {
				ERROR("File system corrupted: invalid characters in name\n");
				goto corrupted;
			}

 * Copyright (c) 2009, 2010, 2011, 2012, 2013, 2019, 2021

			/* check name for invalid characters (i.e /, ., ..) */
			if(check_name(dire->name, dire->size + 1) == FALSE) {
				ERROR("File system corrupted: invalid characters in name\n");
				goto corrupted;
			}

	printf("unsquashfs version 4.4-git (2021/01/17)\n");\
 * Copyright (c) 2009, 2010, 2013, 2014, 2019, 2021

/* unsquash-1234.c */
extern int check_name(char *, int);