Patrick Williams | f1e5d69 | 2016-03-30 15:21:19 -0500 | [diff] [blame^] | 1 | From b1bb4ca6d8777683b6a549fb61dba36759da26f4 Mon Sep 17 00:00:00 2001 |
| 2 | From: Ray Satiro <raysatiro@yahoo.com> |
| 3 | Date: Tue, 26 Jan 2016 23:23:15 +0100 |
| 4 | Subject: [PATCH] curl: avoid local drive traversal when saving file (Windows) |
| 5 | |
| 6 | curl does not sanitize colons in a remote file name that is used as the |
| 7 | local file name. This may lead to a vulnerability on systems where the |
| 8 | colon is a special path character. Currently Windows/DOS is the only OS |
| 9 | where this vulnerability applies. |
| 10 | |
| 11 | CVE-2016-0754 |
| 12 | |
| 13 | Bug: http://curl.haxx.se/docs/adv_20160127B.html |
| 14 | |
| 15 | Upstream-Status: Backport |
| 16 | http://curl.haxx.se/CVE-2016-0754.patch |
| 17 | |
| 18 | CVE: CVE-2016-0754 |
| 19 | Signed-off-by: Armin Kuster <akuster@mvista.com> |
| 20 | |
| 21 | --- |
| 22 | src/tool_cb_hdr.c | 40 ++++++------ |
| 23 | src/tool_doswin.c | 174 ++++++++++++++++++++++++++++++++++++++++++++--------- |
| 24 | src/tool_doswin.h | 2 +- |
| 25 | src/tool_operate.c | 29 ++++++--- |
| 26 | 4 files changed, 187 insertions(+), 58 deletions(-) |
| 27 | |
| 28 | diff --git a/src/tool_cb_hdr.c b/src/tool_cb_hdr.c |
| 29 | index fd208e8..0fca39f 100644 |
| 30 | --- a/src/tool_cb_hdr.c |
| 31 | +++ b/src/tool_cb_hdr.c |
| 32 | @@ -26,10 +26,11 @@ |
| 33 | #define ENABLE_CURLX_PRINTF |
| 34 | /* use our own printf() functions */ |
| 35 | #include "curlx.h" |
| 36 | |
| 37 | #include "tool_cfgable.h" |
| 38 | +#include "tool_doswin.h" |
| 39 | #include "tool_msgs.h" |
| 40 | #include "tool_cb_hdr.h" |
| 41 | |
| 42 | #include "memdebug.h" /* keep this as LAST include */ |
| 43 | |
| 44 | @@ -112,22 +113,28 @@ size_t tool_header_cb(void *ptr, size_t size, size_t nmemb, void *userdata) |
| 45 | /* this expression below typecasts 'cb' only to avoid |
| 46 | warning: signed and unsigned type in conditional expression |
| 47 | */ |
| 48 | len = (ssize_t)cb - (p - str); |
| 49 | filename = parse_filename(p, len); |
| 50 | - if(filename) { |
| 51 | - outs->filename = filename; |
| 52 | - outs->alloc_filename = TRUE; |
| 53 | - outs->is_cd_filename = TRUE; |
| 54 | - outs->s_isreg = TRUE; |
| 55 | - outs->fopened = FALSE; |
| 56 | - outs->stream = NULL; |
| 57 | - hdrcbdata->honor_cd_filename = FALSE; |
| 58 | - break; |
| 59 | - } |
| 60 | - else |
| 61 | + if(!filename) |
| 62 | + return failure; |
| 63 | + |
| 64 | +#if defined(MSDOS) || defined(WIN32) |
| 65 | + if(sanitize_file_name(&filename)) { |
| 66 | + free(filename); |
| 67 | return failure; |
| 68 | + } |
| 69 | +#endif /* MSDOS || WIN32 */ |
| 70 | + |
| 71 | + outs->filename = filename; |
| 72 | + outs->alloc_filename = TRUE; |
| 73 | + outs->is_cd_filename = TRUE; |
| 74 | + outs->s_isreg = TRUE; |
| 75 | + outs->fopened = FALSE; |
| 76 | + outs->stream = NULL; |
| 77 | + hdrcbdata->honor_cd_filename = FALSE; |
| 78 | + break; |
| 79 | } |
| 80 | } |
| 81 | |
| 82 | return cb; |
| 83 | } |
| 84 | @@ -179,19 +186,16 @@ static char *parse_filename(const char *ptr, size_t len) |
| 85 | return NULL; |
| 86 | } |
| 87 | } |
| 88 | |
| 89 | /* scan for the end letter and stop there */ |
| 90 | - q = p; |
| 91 | - while(*q) { |
| 92 | - if(q[1] && (q[0] == '\\')) |
| 93 | - q++; |
| 94 | - else if(q[0] == stop) |
| 95 | + for(q = p; *q; ++q) { |
| 96 | + if(*q == stop) { |
| 97 | + *q = '\0'; |
| 98 | break; |
| 99 | - q++; |
| 100 | + } |
| 101 | } |
| 102 | - *q = '\0'; |
| 103 | |
| 104 | /* make sure the file name doesn't end in \r or \n */ |
| 105 | q = strchr(p, '\r'); |
| 106 | if(q) |
| 107 | *q = '\0'; |
| 108 | diff --git a/src/tool_doswin.c b/src/tool_doswin.c |
| 109 | index dd6e8bb..9c6a7a3 100644 |
| 110 | --- a/src/tool_doswin.c |
| 111 | +++ b/src/tool_doswin.c |
| 112 | @@ -83,46 +83,110 @@ __pragma(warning(pop)) |
| 113 | # define _use_lfn(f) ALWAYS_FALSE /* long file names never available */ |
| 114 | #elif defined(__DJGPP__) |
| 115 | # include <fcntl.h> /* _use_lfn(f) prototype */ |
| 116 | #endif |
| 117 | |
| 118 | -static const char *msdosify (const char *file_name); |
| 119 | -static char *rename_if_dos_device_name (char *file_name); |
| 120 | +static char *msdosify(const char *file_name); |
| 121 | +static char *rename_if_dos_device_name(const char *file_name); |
| 122 | |
| 123 | -/* |
| 124 | - * sanitize_dos_name: returns a newly allocated string holding a |
| 125 | - * valid file name which will be a transformation of given argument |
| 126 | - * in case this wasn't already a valid file name. |
| 127 | - * |
| 128 | - * This function takes ownership of given argument, free'ing it before |
| 129 | - * returning. Caller is responsible of free'ing returned string. Upon |
| 130 | - * out of memory condition function returns NULL. |
| 131 | - */ |
| 132 | |
| 133 | -char *sanitize_dos_name(char *file_name) |
| 134 | +/* |
| 135 | +Sanitize *file_name. |
| 136 | +Success: (CURLE_OK) *file_name points to a sanitized version of the original. |
| 137 | + This function takes ownership of the original *file_name and frees it. |
| 138 | +Failure: (!= CURLE_OK) *file_name is unchanged. |
| 139 | +*/ |
| 140 | +CURLcode sanitize_file_name(char **file_name) |
| 141 | { |
| 142 | - char new_name[PATH_MAX]; |
| 143 | + size_t len; |
| 144 | + char *p, *sanitized; |
| 145 | + |
| 146 | + /* Calculate the maximum length of a filename. |
| 147 | + FILENAME_MAX is often the same as PATH_MAX, in other words it does not |
| 148 | + discount the path information. PATH_MAX size is calculated based on: |
| 149 | + <drive-letter><colon><path-sep><max-filename-len><NULL> */ |
| 150 | + const size_t max_filename_len = PATH_MAX - 3 - 1; |
| 151 | + |
| 152 | + if(!file_name || !*file_name) |
| 153 | + return CURLE_BAD_FUNCTION_ARGUMENT; |
| 154 | + |
| 155 | + len = strlen(*file_name); |
| 156 | + |
| 157 | + if(len >= max_filename_len) |
| 158 | + len = max_filename_len - 1; |
| 159 | |
| 160 | - if(!file_name) |
| 161 | - return NULL; |
| 162 | + sanitized = malloc(len + 1); |
| 163 | |
| 164 | - if(strlen(file_name) >= PATH_MAX) |
| 165 | - file_name[PATH_MAX-1] = '\0'; /* truncate it */ |
| 166 | + if(!sanitized) |
| 167 | + return CURLE_OUT_OF_MEMORY; |
| 168 | |
| 169 | - strcpy(new_name, msdosify(file_name)); |
| 170 | + strncpy(sanitized, *file_name, len); |
| 171 | + sanitized[len] = '\0'; |
| 172 | |
| 173 | - Curl_safefree(file_name); |
| 174 | + for(p = sanitized; *p; ++p ) { |
| 175 | + const char *banned; |
| 176 | + if(1 <= *p && *p <= 31) { |
| 177 | + *p = '_'; |
| 178 | + continue; |
| 179 | + } |
| 180 | + for(banned = "|<>/\\\":?*"; *banned; ++banned) { |
| 181 | + if(*p == *banned) { |
| 182 | + *p = '_'; |
| 183 | + break; |
| 184 | + } |
| 185 | + } |
| 186 | + } |
| 187 | |
| 188 | - return strdup(rename_if_dos_device_name(new_name)); |
| 189 | +#ifdef MSDOS |
| 190 | + /* msdosify checks for more banned characters for MSDOS, however it allows |
| 191 | + for some path information to pass through. since we are sanitizing only a |
| 192 | + filename and cannot allow a path it's important this call be done in |
| 193 | + addition to and not instead of the banned character check above. */ |
| 194 | + p = msdosify(sanitized); |
| 195 | + if(!p) { |
| 196 | + free(sanitized); |
| 197 | + return CURLE_BAD_FUNCTION_ARGUMENT; |
| 198 | + } |
| 199 | + sanitized = p; |
| 200 | + len = strlen(sanitized); |
| 201 | +#endif |
| 202 | + |
| 203 | + p = rename_if_dos_device_name(sanitized); |
| 204 | + if(!p) { |
| 205 | + free(sanitized); |
| 206 | + return CURLE_BAD_FUNCTION_ARGUMENT; |
| 207 | + } |
| 208 | + sanitized = p; |
| 209 | + len = strlen(sanitized); |
| 210 | + |
| 211 | + /* dos_device_name rename will rename a device name, possibly changing the |
| 212 | + length. If the length is too long now we can't truncate it because we |
| 213 | + could end up with a device name. In practice this shouldn't be a problem |
| 214 | + because device names are short, but you never know. */ |
| 215 | + if(len >= max_filename_len) { |
| 216 | + free(sanitized); |
| 217 | + return CURLE_BAD_FUNCTION_ARGUMENT; |
| 218 | + } |
| 219 | + |
| 220 | + *file_name = sanitized; |
| 221 | + return CURLE_OK; |
| 222 | } |
| 223 | |
| 224 | -/* The following functions are taken with modification from the DJGPP |
| 225 | - * port of tar 1.12. They use algorithms originally from DJTAR. */ |
| 226 | +/* The functions msdosify, rename_if_dos_device_name and __crt0_glob_function |
| 227 | + * were taken with modification from the DJGPP port of tar 1.12. They use |
| 228 | + * algorithms originally from DJTAR. |
| 229 | + */ |
| 230 | |
| 231 | -static const char *msdosify (const char *file_name) |
| 232 | +/* |
| 233 | +Extra sanitization MSDOS for file_name. |
| 234 | +Returns a copy of file_name that is sanitized by MSDOS standards. |
| 235 | +Warning: path information may pass through. For sanitizing a filename use |
| 236 | +sanitize_file_name which calls this function after sanitizing path info. |
| 237 | +*/ |
| 238 | +static char *msdosify(const char *file_name) |
| 239 | { |
| 240 | - static char dos_name[PATH_MAX]; |
| 241 | + char dos_name[PATH_MAX]; |
| 242 | static const char illegal_chars_dos[] = ".+, ;=[]" /* illegal in DOS */ |
| 243 | "|<>\\\":?*"; /* illegal in DOS & W95 */ |
| 244 | static const char *illegal_chars_w95 = &illegal_chars_dos[8]; |
| 245 | int idx, dot_idx; |
| 246 | const char *s = file_name; |
| 247 | @@ -199,39 +263,89 @@ static const char *msdosify (const char *file_name) |
| 248 | else |
| 249 | idx++; |
| 250 | } |
| 251 | |
| 252 | *d = '\0'; |
| 253 | - return dos_name; |
| 254 | + return strdup(dos_name); |
| 255 | } |
| 256 | |
| 257 | -static char *rename_if_dos_device_name (char *file_name) |
| 258 | +/* |
| 259 | +Rename file_name if it's a representation of a device name. |
| 260 | +Returns a copy of file_name, and the copy will have contents different from the |
| 261 | +original if a device name was found. |
| 262 | +*/ |
| 263 | +static char *rename_if_dos_device_name(const char *file_name) |
| 264 | { |
| 265 | /* We could have a file whose name is a device on MS-DOS. Trying to |
| 266 | * retrieve such a file would fail at best and wedge us at worst. We need |
| 267 | * to rename such files. */ |
| 268 | - char *base; |
| 269 | + char *p, *base; |
| 270 | struct_stat st_buf; |
| 271 | char fname[PATH_MAX]; |
| 272 | |
| 273 | strncpy(fname, file_name, PATH_MAX-1); |
| 274 | fname[PATH_MAX-1] = '\0'; |
| 275 | base = basename(fname); |
| 276 | if(((stat(base, &st_buf)) == 0) && (S_ISCHR(st_buf.st_mode))) { |
| 277 | size_t blen = strlen(base); |
| 278 | |
| 279 | - if(strlen(fname) >= PATH_MAX-1) { |
| 280 | + if(strlen(fname) == PATH_MAX-1) { |
| 281 | /* Make room for the '_' */ |
| 282 | blen--; |
| 283 | base[blen] = '\0'; |
| 284 | } |
| 285 | /* Prepend a '_'. */ |
| 286 | memmove(base + 1, base, blen + 1); |
| 287 | base[0] = '_'; |
| 288 | - strcpy(file_name, fname); |
| 289 | } |
| 290 | - return file_name; |
| 291 | + |
| 292 | + /* The above stat check does not identify devices for me in Windows 7. For |
| 293 | + example a stat on COM1 returns a regular file S_IFREG. According to MSDN |
| 294 | + stat doc that is the correct behavior, so I assume the above code is |
| 295 | + legacy, maybe MSDOS or DJGPP specific? */ |
| 296 | + |
| 297 | + /* Rename devices. |
| 298 | + Examples: CON => _CON, CON.EXT => CON_EXT, CON:ADS => CON_ADS */ |
| 299 | + for(p = fname; p; p = (p == fname && fname != base ? base : NULL)) { |
| 300 | + size_t p_len; |
| 301 | + int x = (curl_strnequal(p, "CON", 3) || |
| 302 | + curl_strnequal(p, "PRN", 3) || |
| 303 | + curl_strnequal(p, "AUX", 3) || |
| 304 | + curl_strnequal(p, "NUL", 3)) ? 3 : |
| 305 | + (curl_strnequal(p, "CLOCK$", 6)) ? 6 : |
| 306 | + (curl_strnequal(p, "COM", 3) || curl_strnequal(p, "LPT", 3)) ? |
| 307 | + (('1' <= p[3] && p[3] <= '9') ? 4 : 3) : 0; |
| 308 | + |
| 309 | + if(!x) |
| 310 | + continue; |
| 311 | + |
| 312 | + /* the devices may be accessible with an extension or ADS, for |
| 313 | + example CON.AIR and CON:AIR both access console */ |
| 314 | + if(p[x] == '.' || p[x] == ':') { |
| 315 | + p[x] = '_'; |
| 316 | + continue; |
| 317 | + } |
| 318 | + else if(p[x]) /* no match */ |
| 319 | + continue; |
| 320 | + |
| 321 | + p_len = strlen(p); |
| 322 | + |
| 323 | + if(strlen(fname) == PATH_MAX-1) { |
| 324 | + /* Make room for the '_' */ |
| 325 | + p_len--; |
| 326 | + p[p_len] = '\0'; |
| 327 | + } |
| 328 | + /* Prepend a '_'. */ |
| 329 | + memmove(p + 1, p, p_len + 1); |
| 330 | + p[0] = '_'; |
| 331 | + |
| 332 | + /* if fname was just modified then the basename pointer must be updated */ |
| 333 | + if(p == fname) |
| 334 | + base = basename(fname); |
| 335 | + } |
| 336 | + |
| 337 | + return strdup(fname); |
| 338 | } |
| 339 | |
| 340 | #if defined(MSDOS) && (defined(__DJGPP__) || defined(__GO32__)) |
| 341 | |
| 342 | /* |
| 343 | diff --git a/src/tool_doswin.h b/src/tool_doswin.h |
| 344 | index cd216db..fc83f16 100644 |
| 345 | --- a/src/tool_doswin.h |
| 346 | +++ b/src/tool_doswin.h |
| 347 | @@ -23,11 +23,11 @@ |
| 348 | ***************************************************************************/ |
| 349 | #include "tool_setup.h" |
| 350 | |
| 351 | #if defined(MSDOS) || defined(WIN32) |
| 352 | |
| 353 | -char *sanitize_dos_name(char *file_name); |
| 354 | +CURLcode sanitize_file_name(char **filename); |
| 355 | |
| 356 | #if defined(MSDOS) && (defined(__DJGPP__) || defined(__GO32__)) |
| 357 | |
| 358 | char **__crt0_glob_function(char *arg); |
| 359 | |
| 360 | diff --git a/src/tool_operate.c b/src/tool_operate.c |
| 361 | index 30d60cb..272ebd4 100644 |
| 362 | --- a/src/tool_operate.c |
| 363 | +++ b/src/tool_operate.c |
| 364 | @@ -541,30 +541,41 @@ static CURLcode operate_do(struct GlobalConfig *global, |
| 365 | if(!outfile) { |
| 366 | /* extract the file name from the URL */ |
| 367 | result = get_url_file_name(&outfile, this_url); |
| 368 | if(result) |
| 369 | goto show_error; |
| 370 | + |
| 371 | +#if defined(MSDOS) || defined(WIN32) |
| 372 | + result = sanitize_file_name(&outfile); |
| 373 | + if(result) { |
| 374 | + Curl_safefree(outfile); |
| 375 | + goto show_error; |
| 376 | + } |
| 377 | +#endif /* MSDOS || WIN32 */ |
| 378 | + |
| 379 | if(!*outfile && !config->content_disposition) { |
| 380 | helpf(global->errors, "Remote file name has no length!\n"); |
| 381 | result = CURLE_WRITE_ERROR; |
| 382 | goto quit_urls; |
| 383 | } |
| 384 | -#if defined(MSDOS) || defined(WIN32) |
| 385 | - /* For DOS and WIN32, we do some major replacing of |
| 386 | - bad characters in the file name before using it */ |
| 387 | - outfile = sanitize_dos_name(outfile); |
| 388 | - if(!outfile) { |
| 389 | - result = CURLE_OUT_OF_MEMORY; |
| 390 | - goto show_error; |
| 391 | - } |
| 392 | -#endif /* MSDOS || WIN32 */ |
| 393 | } |
| 394 | else if(urls) { |
| 395 | /* fill '#1' ... '#9' terms from URL pattern */ |
| 396 | char *storefile = outfile; |
| 397 | result = glob_match_url(&outfile, storefile, urls); |
| 398 | Curl_safefree(storefile); |
| 399 | + |
| 400 | +#if defined(MSDOS) || defined(WIN32) |
| 401 | + if(!result) { |
| 402 | + result = sanitize_file_name(&outfile); |
| 403 | + if(result) { |
| 404 | + Curl_safefree(outfile); |
| 405 | + goto show_error; |
| 406 | + } |
| 407 | + } |
| 408 | +#endif /* MSDOS || WIN32 */ |
| 409 | + |
| 410 | if(result) { |
| 411 | /* bad globbing */ |
| 412 | warnf(config->global, "bad output glob!\n"); |
| 413 | goto quit_urls; |
| 414 | } |
| 415 | -- |
| 416 | 2.7.0 |
| 417 | |