From b1bb4ca6d8777683b6a549fb61dba36759da26f4 Mon Sep 17 00:00:00 2001
From: Ray Satiro <raysatiro@yahoo.com>
Date: Tue, 26 Jan 2016 23:23:15 +0100
Subject: [PATCH] curl: avoid local drive traversal when saving file (Windows)

curl does not sanitize colons in a remote file name that is used as the
local file name. This may lead to a vulnerability on systems where the
colon is a special path character. Currently Windows/DOS is the only OS
where this vulnerability applies.

CVE-2016-0754

Bug: http://curl.haxx.se/docs/adv_20160127B.html

Upstream-Status: Backport
http://curl.haxx.se/CVE-2016-0754.patch

CVE: CVE-2016-0754
Signed-off-by: Armin Kuster <akuster@mvista.com>

---
 src/tool_cb_hdr.c  |  40 ++++++------
 src/tool_doswin.c  | 174 ++++++++++++++++++++++++++++++++++++++++++++---------
 src/tool_doswin.h  |   2 +-
 src/tool_operate.c |  29 ++++++---
 4 files changed, 187 insertions(+), 58 deletions(-)

diff --git a/src/tool_cb_hdr.c b/src/tool_cb_hdr.c
index fd208e8..0fca39f 100644
--- a/src/tool_cb_hdr.c
+++ b/src/tool_cb_hdr.c
@@ -26,10 +26,11 @@
 #define ENABLE_CURLX_PRINTF
 /* use our own printf() functions */
 #include "curlx.h"
 
 #include "tool_cfgable.h"
+#include "tool_doswin.h"
 #include "tool_msgs.h"
 #include "tool_cb_hdr.h"
 
 #include "memdebug.h" /* keep this as LAST include */
 
@@ -112,22 +113,28 @@ size_t tool_header_cb(void *ptr, size_t size, size_t nmemb, void *userdata)
       /* this expression below typecasts 'cb' only to avoid
          warning: signed and unsigned type in conditional expression
       */
       len = (ssize_t)cb - (p - str);
       filename = parse_filename(p, len);
-      if(filename) {
-        outs->filename = filename;
-        outs->alloc_filename = TRUE;
-        outs->is_cd_filename = TRUE;
-        outs->s_isreg = TRUE;
-        outs->fopened = FALSE;
-        outs->stream = NULL;
-        hdrcbdata->honor_cd_filename = FALSE;
-        break;
-      }
-      else
+      if(!filename)
+        return failure;
+
+#if defined(MSDOS) || defined(WIN32)
+      if(sanitize_file_name(&filename)) {
+        free(filename);
         return failure;
+      }
+#endif /* MSDOS || WIN32 */
+
+      outs->filename = filename;
+      outs->alloc_filename = TRUE;
+      outs->is_cd_filename = TRUE;
+      outs->s_isreg = TRUE;
+      outs->fopened = FALSE;
+      outs->stream = NULL;
+      hdrcbdata->honor_cd_filename = FALSE;
+      break;
     }
   }
 
   return cb;
 }
@@ -179,19 +186,16 @@ static char *parse_filename(const char *ptr, size_t len)
       return NULL;
     }
   }
 
   /* scan for the end letter and stop there */
-  q = p;
-  while(*q) {
-    if(q[1] && (q[0] == '\\'))
-      q++;
-    else if(q[0] == stop)
+  for(q = p; *q; ++q) {
+    if(*q == stop) {
+      *q = '\0';
       break;
-    q++;
+    }
   }
-  *q = '\0';
 
   /* make sure the file name doesn't end in \r or \n */
   q = strchr(p, '\r');
   if(q)
     *q = '\0';
diff --git a/src/tool_doswin.c b/src/tool_doswin.c
index dd6e8bb..9c6a7a3 100644
--- a/src/tool_doswin.c
+++ b/src/tool_doswin.c
@@ -83,46 +83,110 @@ __pragma(warning(pop))
 #  define _use_lfn(f) ALWAYS_FALSE  /* long file names never available */
 #elif defined(__DJGPP__)
 #  include <fcntl.h>                /* _use_lfn(f) prototype */
 #endif
 
-static const char *msdosify (const char *file_name);
-static char *rename_if_dos_device_name (char *file_name);
+static char *msdosify(const char *file_name);
+static char *rename_if_dos_device_name(const char *file_name);
 
-/*
- * sanitize_dos_name: returns a newly allocated string holding a
- * valid file name which will be a transformation of given argument
- * in case this wasn't already a valid file name.
- *
- * This function takes ownership of given argument, free'ing it before
- * returning. Caller is responsible of free'ing returned string. Upon
- * out of memory condition function returns NULL.
- */
 
-char *sanitize_dos_name(char *file_name)
+/*
+Sanitize *file_name.
+Success: (CURLE_OK) *file_name points to a sanitized version of the original.
+         This function takes ownership of the original *file_name and frees it.
+Failure: (!= CURLE_OK) *file_name is unchanged.
+*/
+CURLcode sanitize_file_name(char **file_name)
 {
-  char new_name[PATH_MAX];
+  size_t len;
+  char *p, *sanitized;
+
+  /* Calculate the maximum length of a filename.
+     FILENAME_MAX is often the same as PATH_MAX, in other words it does not
+     discount the path information. PATH_MAX size is calculated based on:
+     <drive-letter><colon><path-sep><max-filename-len><NULL> */
+  const size_t max_filename_len = PATH_MAX - 3 - 1;
+
+  if(!file_name || !*file_name)
+    return CURLE_BAD_FUNCTION_ARGUMENT;
+
+  len = strlen(*file_name);
+
+  if(len >= max_filename_len)
+    len = max_filename_len - 1;
 
-  if(!file_name)
-    return NULL;
+  sanitized = malloc(len + 1);
 
-  if(strlen(file_name) >= PATH_MAX)
-    file_name[PATH_MAX-1] = '\0'; /* truncate it */
+  if(!sanitized)
+    return CURLE_OUT_OF_MEMORY;
 
-  strcpy(new_name, msdosify(file_name));
+  strncpy(sanitized, *file_name, len);
+  sanitized[len] = '\0';
 
-  Curl_safefree(file_name);
+  for(p = sanitized; *p; ++p ) {
+    const char *banned;
+    if(1 <= *p && *p <= 31) {
+      *p = '_';
+      continue;
+    }
+    for(banned = "|<>/\\\":?*"; *banned; ++banned) {
+      if(*p == *banned) {
+        *p = '_';
+        break;
+      }
+    }
+  }
 
-  return strdup(rename_if_dos_device_name(new_name));
+#ifdef MSDOS
+  /* msdosify checks for more banned characters for MSDOS, however it allows
+     for some path information to pass through. since we are sanitizing only a
+     filename and cannot allow a path it's important this call be done in
+     addition to and not instead of the banned character check above. */
+  p = msdosify(sanitized);
+  if(!p) {
+    free(sanitized);
+    return CURLE_BAD_FUNCTION_ARGUMENT;
+  }
+  sanitized = p;
+  len = strlen(sanitized);
+#endif
+
+  p = rename_if_dos_device_name(sanitized);
+  if(!p) {
+    free(sanitized);
+    return CURLE_BAD_FUNCTION_ARGUMENT;
+  }
+  sanitized = p;
+  len = strlen(sanitized);
+
+  /* dos_device_name rename will rename a device name, possibly changing the
+     length. If the length is too long now we can't truncate it because we
+     could end up with a device name. In practice this shouldn't be a problem
+     because device names are short, but you never know. */
+  if(len >= max_filename_len) {
+    free(sanitized);
+    return CURLE_BAD_FUNCTION_ARGUMENT;
+  }
+
+  *file_name = sanitized;
+  return CURLE_OK;
 }
 
-/* The following functions are taken with modification from the DJGPP
- * port of tar 1.12. They use algorithms originally from DJTAR. */
+/* The functions msdosify, rename_if_dos_device_name and __crt0_glob_function
+ * were taken with modification from the DJGPP port of tar 1.12. They use
+ * algorithms originally from DJTAR.
+ */
 
-static const char *msdosify (const char *file_name)
+/*
+Extra sanitization MSDOS for file_name.
+Returns a copy of file_name that is sanitized by MSDOS standards.
+Warning: path information may pass through. For sanitizing a filename use
+sanitize_file_name which calls this function after sanitizing path info.
+*/
+static char *msdosify(const char *file_name)
 {
-  static char dos_name[PATH_MAX];
+  char dos_name[PATH_MAX];
   static const char illegal_chars_dos[] = ".+, ;=[]" /* illegal in DOS */
     "|<>\\\":?*"; /* illegal in DOS & W95 */
   static const char *illegal_chars_w95 = &illegal_chars_dos[8];
   int idx, dot_idx;
   const char *s = file_name;
@@ -199,39 +263,89 @@ static const char *msdosify (const char *file_name)
     else
       idx++;
   }
 
   *d = '\0';
-  return dos_name;
+  return strdup(dos_name);
 }
 
-static char *rename_if_dos_device_name (char *file_name)
+/*
+Rename file_name if it's a representation of a device name.
+Returns a copy of file_name, and the copy will have contents different from the
+original if a device name was found.
+*/
+static char *rename_if_dos_device_name(const char *file_name)
 {
   /* We could have a file whose name is a device on MS-DOS.  Trying to
    * retrieve such a file would fail at best and wedge us at worst.  We need
    * to rename such files. */
-  char *base;
+  char *p, *base;
   struct_stat st_buf;
   char fname[PATH_MAX];
 
   strncpy(fname, file_name, PATH_MAX-1);
   fname[PATH_MAX-1] = '\0';
   base = basename(fname);
   if(((stat(base, &st_buf)) == 0) && (S_ISCHR(st_buf.st_mode))) {
     size_t blen = strlen(base);
 
-    if(strlen(fname) >= PATH_MAX-1) {
+    if(strlen(fname) == PATH_MAX-1) {
       /* Make room for the '_' */
       blen--;
       base[blen] = '\0';
     }
     /* Prepend a '_'.  */
     memmove(base + 1, base, blen + 1);
     base[0] = '_';
-    strcpy(file_name, fname);
   }
-  return file_name;
+
+  /* The above stat check does not identify devices for me in Windows 7. For
+     example a stat on COM1 returns a regular file S_IFREG. According to MSDN
+     stat doc that is the correct behavior, so I assume the above code is
+     legacy, maybe MSDOS or DJGPP specific? */
+
+  /* Rename devices.
+     Examples: CON => _CON, CON.EXT => CON_EXT, CON:ADS => CON_ADS */
+  for(p = fname; p; p = (p == fname && fname != base ? base : NULL)) {
+    size_t p_len;
+    int x = (curl_strnequal(p, "CON", 3) ||
+             curl_strnequal(p, "PRN", 3) ||
+             curl_strnequal(p, "AUX", 3) ||
+             curl_strnequal(p, "NUL", 3)) ? 3 :
+            (curl_strnequal(p, "CLOCK$", 6)) ? 6 :
+            (curl_strnequal(p, "COM", 3) || curl_strnequal(p, "LPT", 3)) ?
+              (('1' <= p[3] && p[3] <= '9') ? 4 : 3) : 0;
+
+    if(!x)
+      continue;
+
+    /* the devices may be accessible with an extension or ADS, for
+       example CON.AIR and CON:AIR both access console */
+    if(p[x] == '.' || p[x] == ':') {
+      p[x] = '_';
+      continue;
+    }
+    else if(p[x]) /* no match */
+      continue;
+
+    p_len = strlen(p);
+
+    if(strlen(fname) == PATH_MAX-1) {
+      /* Make room for the '_' */
+      p_len--;
+      p[p_len] = '\0';
+    }
+    /* Prepend a '_'.  */
+    memmove(p + 1, p, p_len + 1);
+    p[0] = '_';
+
+    /* if fname was just modified then the basename pointer must be updated */
+    if(p == fname)
+      base = basename(fname);
+  }
+
+  return strdup(fname);
 }
 
 #if defined(MSDOS) && (defined(__DJGPP__) || defined(__GO32__))
 
 /*
diff --git a/src/tool_doswin.h b/src/tool_doswin.h
index cd216db..fc83f16 100644
--- a/src/tool_doswin.h
+++ b/src/tool_doswin.h
@@ -23,11 +23,11 @@
  ***************************************************************************/
 #include "tool_setup.h"
 
 #if defined(MSDOS) || defined(WIN32)
 
-char *sanitize_dos_name(char *file_name);
+CURLcode sanitize_file_name(char **filename);
 
 #if defined(MSDOS) && (defined(__DJGPP__) || defined(__GO32__))
 
 char **__crt0_glob_function(char *arg);
 
diff --git a/src/tool_operate.c b/src/tool_operate.c
index 30d60cb..272ebd4 100644
--- a/src/tool_operate.c
+++ b/src/tool_operate.c
@@ -541,30 +541,41 @@ static CURLcode operate_do(struct GlobalConfig *global,
           if(!outfile) {
             /* extract the file name from the URL */
             result = get_url_file_name(&outfile, this_url);
             if(result)
               goto show_error;
+
+#if defined(MSDOS) || defined(WIN32)
+            result = sanitize_file_name(&outfile);
+            if(result) {
+              Curl_safefree(outfile);
+              goto show_error;
+            }
+#endif /* MSDOS || WIN32 */
+
             if(!*outfile && !config->content_disposition) {
               helpf(global->errors, "Remote file name has no length!\n");
               result = CURLE_WRITE_ERROR;
               goto quit_urls;
             }
-#if defined(MSDOS) || defined(WIN32)
-            /* For DOS and WIN32, we do some major replacing of
-               bad characters in the file name before using it */
-            outfile = sanitize_dos_name(outfile);
-            if(!outfile) {
-              result = CURLE_OUT_OF_MEMORY;
-              goto show_error;
-            }
-#endif /* MSDOS || WIN32 */
           }
           else if(urls) {
             /* fill '#1' ... '#9' terms from URL pattern */
             char *storefile = outfile;
             result = glob_match_url(&outfile, storefile, urls);
             Curl_safefree(storefile);
+
+#if defined(MSDOS) || defined(WIN32)
+            if(!result) {
+              result = sanitize_file_name(&outfile);
+              if(result) {
+                Curl_safefree(outfile);
+                goto show_error;
+              }
+            }
+#endif /* MSDOS || WIN32 */
+
             if(result) {
               /* bad globbing */
               warnf(config->global, "bad output glob!\n");
               goto quit_urls;
             }
-- 
2.7.0