CVE-2018-18586 (published 2018-10-23T02:29Z)

chmextract.c in the chmextract sample program, as distributed with libmspack before 0.8alpha, does not protect against absolute/relative pathnames in CHM files, leading to Directory Traversal. NOTE: the vendor disputes that this is a libmspack vulnerability, because chmextract.c was only intended as a source-code example, not a supported application

Severity: MEDIUM
CVSS v2 base score: 5.0 (AV:N/AC:L/Au:N/C:P/I:N/A:N)
CVSS v3 base score: 5.3, MEDIUM (CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N)

Weakness information:
cwe_id: CWE-22
cwe_name: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
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.
extended_description: 
            Many file operations are intended to take place within a restricted directory. By using special elements such as ".." and "/" separators, attackers can escape outside of the restricted location to access files or directories that are elsewhere on the system. One of the most common special elements is the "../" sequence, which in most modern operating systems is interpreted as the parent directory of the current location. This is referred to as relative path traversal. Path traversal also covers the use of absolute pathnames such as "/usr/local/bin" to access unexpected files. This is referred to as absolute path traversal.
         
         

Fix information:
repo_url: https://github.com/kyz/libmspack
commit_details: repo_url: https://github.com/kyz/libmspack
author: Stuart Caie
author_date: 2018-10-20 19:06:32+01:00
msg: add anti "../" and leading slash protection to chmextract
repository: repo_url: https://github.com/kyz/libmspack
repo_name: kyz/libmspack
description: A library for some loosely related Microsoft compression formats, CAB, CHM, HLP, LIT, KWAJ and SZDD.
repo_language: C
owner: kyz
file_changes: filename: ChangeLog
change_type: ModificationType.MODIFY
diff: @@ -1,3 +1,13 @@
+2018-10-20  Stuart Caie <kyzer@cabextract.org.uk>
+
+	* src/chmextract.c: add anti "../" and leading slash protection to
+	chmextract. I'm not pleased about this. All the sample code provided
+	with libmspack is meant to be simple examples of library use, not
+	"productised" binaries. Making the "useful" code samples install
+	as binaries was a mistake. They were never intended to protect you
+	from unpacking archive files with relative/absolute paths, and I
+	would prefer that they never will be.
+
 2018-10-17  Stuart Caie <kyzer@cabextract.org.uk>
 
 	* cab.h: Make the CAB block input buffer one byte larger, to allow

programming_language: Markdown
filename: chmextract.c
change_type: ModificationType.MODIFY
diff: @@ -25,8 +25,6 @@
 
 mode_t user_umask;
 
-#define FILENAME ".test.chmx"
-
 /**
  * Ensures that all directory components in a filepath exist. New directory
  * components are created, if necessary.
@@ -51,126 +49,22 @@ static int ensure_filepath(char *path) {
   return 1;
 }
 
-/**
- * Creates a UNIX filename from the internal CAB filename and the given
- * parameters.
- *
- * @param fname  the internal CAB filename.
- * @param dir    a directory path to prepend to the output filename.
- * @param lower  if non-zero, filename should be made lower-case.
- * @param isunix if zero, MS-DOS path seperators are used in the internal
- *               CAB filename. If non-zero, UNIX path seperators are used.
- * @param utf8   if non-zero, the internal CAB filename is encoded in UTF8.
- * @return a freshly allocated and created filename, or NULL if there was
- *         not enough memory.
- * @see unix_path_seperators()
- */
-static char *create_output_name(unsigned char *fname, unsigned char *dir,
-			 int lower, int isunix, int utf8)
-{
-  unsigned char *p, *name, c, *fe, sep, slash;
-  unsigned int x;
-
-  sep   = (isunix) ? '/'  : '\\'; /* the path-seperator */
-  slash = (isunix) ? '\\' : '/';  /* the other slash */
-
-  /* length of filename */
-  x = strlen((char *) fname);
-  /* UTF8 worst case scenario: tolower() expands all chars from 1 to 3 bytes */
-  if (utf8) x *= 3;
-  /* length of output directory */
-  if (dir) x += strlen((char *) dir);
-
-  if (!(name = (unsigned char *) malloc(x + 2))) {
-    fprintf(stderr, "out of memory!\n");
-    return NULL;
-  }
-  
-  /* start with blank name */
-  *name = '\0';
-
-  /* add output directory if needed */
-  if (dir) {
-    strcpy((char *) name, (char *) dir);
-    strcat((char *) name, "/");
-  }
-
-  /* remove leading slashes */
-  while (*fname == sep) fname++;
-
-  /* copy from fi->filename to new name, converting MS-DOS slashes to UNIX
-   * slashes as we go. Also lowercases characters if needed.
-   */
-  p = &name[strlen((char *)name)];
-  fe = &fname[strlen((char *)fname)];
-
-  if (utf8) {
-    /* UTF8 translates two-byte unicode characters into 1, 2 or 3 bytes.
-     * %000000000xxxxxxx -> %0xxxxxxx
-     * %00000xxxxxyyyyyy -> %110xxxxx %10yyyyyy
-     * %xxxxyyyyyyzzzzzz -> %1110xxxx %10yyyyyy %10zzzzzz
-     *
-     * Therefore, the inverse is as follows:
-     * First char:
-     *  0x00 - 0x7F = one byte char
-     *  0x80 - 0xBF = invalid
-     *  0xC0 - 0xDF = 2 byte char (next char only 0x80-0xBF is valid)
-     *  0xE0 - 0xEF = 3 byte char (next 2 chars only 0x80-0xBF is valid)
-     *  0xF0 - 0xFF = invalid
-     */
-    do {
-      if (fname >= fe) {
-	free(name);
-	return NULL;
-      }
-
-      /* get next UTF8 char */
-      if ((c = *fname++) < 0x80) x = c;
-      else {
-	if ((c >= 0xC0) && (c < 0xE0)) {
-	  x = (c & 0x1F) << 6;
-	  x |= *fname++ & 0x3F;
-	}
-	else if ((c >= 0xE0) && (c < 0xF0)) {
-	  x = (c & 0xF) << 12;
-	  x |= (*fname++ & 0x3F) << 6;
-	  x |= *fname++ & 0x3F;
-	}
-	else x = '?';
-      }
-
-      /* whatever is the path seperator -> '/'
-       * whatever is the other slash    -> '\\'
-       * otherwise, if lower is set, the lowercase version */
-      if      (x == sep)   x = '/';
-      else if (x == slash) x = '\\';
-      else if (lower)      x = (unsigned int) tolower((int) x);
-
-      /* integer back to UTF8 */
-      if (x < 0x80) {
-	*p++ = (unsigned char) x;
-      }
-      else if (x < 0x800) {
-	*p++ = 0xC0 | (x >> 6);   
-	*p++ = 0x80 | (x & 0x3F);
-      }
-      else {
-	*p++ = 0xE0 | (x >> 12);
-	*p++ = 0x80 | ((x >> 6) & 0x3F);
-	*p++ = 0x80 | (x & 0x3F);
-      }
-    } while (x);
-  }
-  else {
-    /* regular non-utf8 version */
-    do {
-      c = *fname++;
-      if      (c == sep)   c = '/';
-      else if (c == slash) c = '\\';
-      else if (lower)      c = (unsigned char) tolower((int) c);
-    } while ((*p++ = c));
-  }
-  return (char *) name;
+char *create_output_name(char *fname) {
+    char *out, *p;
+    if ((out = malloc(strlen(fname) + 1))) {
+        /* remove leading slashes */
+        while (*fname == '/' || *fname == '\\') fname++;
+        /* if that removes all characters, just call it "x" */
+        strcpy(out, (*fname) ? fname : "x");
+
+        /* change "../" to "xx/" */
+        for (p = out; *p; p++) {
+            if (p[0] == '.' && p[1] == '.' && (p[2] == '/' || p[2] == '\\')) {
+               p[0] = p[1] = 'x';
+            }
+        }
+    }
+    return out;
 }
 
 static int sortfunc(const void *a, const void *b) {
@@ -205,7 +99,7 @@ int main(int argc, char *argv[]) {
 	  qsort(f, numf, sizeof(struct mschmd_file *), &sortfunc);
 
 	  for (i = 0; i < numf; i++) {
-	    char *outname = create_output_name((unsigned char *)f[i]->filename,NULL,0,1,0);
+	    char *outname = create_output_name(f[i]->filename);
 	    printf("Extracting %s\n", outname);
 	    ensure_filepath(outname);
 	    if (chmd->extract(chmd, f[i], outname)) {

programming_language: C