X-Git-Url: https://git.exim.org/exim.git/blobdiff_plain/4fd5d2bf25195969b9c6a6c23a59c495400ece8d..44de51a174765f7f5ecb250638cdb3fe64ec67dc:/src/src/mime.c diff --git a/src/src/mime.c b/src/src/mime.c index 95d3da472..5d3505a7c 100644 --- a/src/src/mime.c +++ b/src/src/mime.c @@ -2,8 +2,10 @@ * Exim - an Internet mail transport agent * *************************************************/ -/* Copyright (c) Tom Kistner 2004 */ -/* License: GPL */ +/* Copyright (c) Tom Kistner 2004, 2015 + * License: GPL + * Copyright (c) The Exim Maintainers 2017 + */ #include "exim.h" #ifdef WITH_CONTENT_SCAN /* entire file */ @@ -13,19 +15,45 @@ FILE *mime_stream = NULL; uschar *mime_current_boundary = NULL; +static mime_header mime_header_list[] = { + /* name namelen value */ + { US"content-type:", 13, &mime_content_type }, + { US"content-disposition:", 20, &mime_content_disposition }, + { US"content-transfer-encoding:", 26, &mime_content_transfer_encoding }, + { US"content-id:", 11, &mime_content_id }, + { US"content-description:", 20, &mime_content_description } +}; + +static int mime_header_list_size = nelem(mime_header_list); + +static mime_parameter mime_parameter_list[] = { + /* name namelen value */ + { US"name=", 5, &mime_filename }, + { US"filename=", 9, &mime_filename }, + { US"charset=", 8, &mime_charset }, + { US"boundary=", 9, &mime_boundary } +}; + + /************************************************* * set MIME anomaly level + text * *************************************************/ /* Small wrapper to set the two expandables which give info on detected "problems" in MIME - encodings. Those are defined in mime.h. */ + encodings. Indexes are defined in mime.h. */ void -mime_set_anomaly(int level, const char *text) +mime_set_anomaly(int idx) { - mime_anomaly_level = level; - mime_anomaly_text = CUS text; +struct anom { + int level; + const uschar * text; +} anom[] = { {1, CUS"Broken Quoted-Printable encoding detected"}, + {2, CUS"Broken BASE64 encoding detected"} }; + +mime_anomaly_level = anom[idx].level; +mime_anomaly_text = anom[idx].text; } @@ -41,7 +69,7 @@ mime_set_anomaly(int level, const char *text) 0-255 - char to write */ -uschar * +static uschar * mime_decode_qp_char(uschar *qp_p, int *c) { uschar *initial_pos = qp_p; @@ -99,84 +127,6 @@ mime_decode_asis(FILE* in, FILE* out, uschar* boundary) } -/* decode base64 MIME part */ -static ssize_t -mime_decode_base64(FILE* in, FILE* out, uschar* boundary) -{ - uschar ibuf[MIME_MAX_LINE_LENGTH], obuf[MIME_MAX_LINE_LENGTH]; - uschar *ipos, *opos; - ssize_t len, size = 0; - int bytestate = 0; - - opos = obuf; - - while (Ufgets(ibuf, MIME_MAX_LINE_LENGTH, in) != NULL) - { - if (boundary != NULL - && Ustrncmp(ibuf, "--", 2) == 0 - && Ustrncmp((ibuf+2), boundary, Ustrlen(boundary)) == 0 - ) - break; - - for (ipos = ibuf ; *ipos != '\r' && *ipos != '\n' && *ipos != 0; ++ipos) - { - if (*ipos == '=') /* skip padding */ - { - ++bytestate; - continue; - } - if (mime_b64[*ipos] == 128) /* skip bad characters */ - { - mime_set_anomaly(MIME_ANOMALY_BROKEN_BASE64); - continue; - } - - /* simple state-machine */ - switch((bytestate++) & 3) - { - case 0: - *opos = mime_b64[*ipos] << 2; - break; - case 1: - *opos |= mime_b64[*ipos] >> 4; - ++opos; - *opos = mime_b64[*ipos] << 4; - break; - case 2: - *opos |= mime_b64[*ipos] >> 2; - ++opos; - *opos = mime_b64[*ipos] << 6; - break; - case 3: - *opos |= mime_b64[*ipos]; - ++opos; - break; - } /* switch */ - } /* for */ - - /* something to write? */ - len = opos - obuf; - if (len > 0) - { - if (fwrite(obuf, 1, len, out) != len) return -1; /* error */ - size += len; - /* copy incomplete last byte to start of obuf, where we continue */ - if ((bytestate & 3) != 0) - *obuf = *opos; - opos = obuf; - } - } /* while */ - - /* write out last byte if it was incomplete */ - if (bytestate & 3) - { - if (fwrite(obuf, 1, 1, out) != 1) return -1; - ++size; - } - - return size; -} - /* decode quoted-printable MIME part */ static ssize_t @@ -209,24 +159,16 @@ while (fgets(CS ibuf, MIME_MAX_LINE_LENGTH, in) != NULL) { /* Error from decoder. ipos is unchanged. */ mime_set_anomaly(MIME_ANOMALY_BROKEN_QP); - *opos = '='; - ++opos; + *opos++ = '='; ++ipos; } else if (decode_qp_result == -1) break; else if (decode_qp_result >= 0) - { - *opos = decode_qp_result; - ++opos; - } + *opos++ = decode_qp_result; } else - { - *opos = *ipos; - ++opos; - ++ipos; - } + *opos++ = *ipos++; } /* something to write? */ len = opos - obuf; @@ -240,21 +182,17 @@ return size; } -FILE * +/* + * Return open filehandle for combo of path and file. + * Side-effect: set mime_decoded_filename, to copy in allocated mem + */ +static FILE * mime_get_decode_file(uschar *pname, uschar *fname) { -FILE *f = NULL; -uschar *filename; - -filename = (uschar *)malloc(2048); - if (pname && fname) - { - (void)string_format(filename, 2048, "%s/%s", pname, fname); - f = modefopen(filename,"wb+",SPOOL_MODE); - } + mime_decoded_filename = string_sprintf("%s/%s", pname, fname); else if (!pname) - f = modefopen(fname,"wb+",SPOOL_MODE); + mime_decoded_filename = string_copy(fname); else if (!fname) { int file_nr = 0; @@ -264,52 +202,41 @@ else if (!fname) do { struct stat mystat; - (void)string_format(filename, 2048, - "%s/%s-%05u", pname, message_id, file_nr++); + mime_decoded_filename = string_sprintf("%s/%s-%05u", pname, message_id, file_nr++); /* security break */ if (file_nr >= 1024) break; - result = stat(CS filename, &mystat); + result = stat(CS mime_decoded_filename, &mystat); } while(result != -1); - - f = modefopen(filename, "wb+", SPOOL_MODE); } -/* set expansion variable */ -mime_decoded_filename = filename; - -return f; +return modefopen(mime_decoded_filename, "wb+", SPOOL_MODE); } int -mime_decode(uschar **listptr) +mime_decode(const uschar **listptr) { int sep = 0; -uschar *list = *listptr; -uschar *option; -uschar option_buffer[1024]; -uschar decode_path[1024]; +const uschar *list = *listptr; +uschar * option; +uschar * decode_path; FILE *decode_file = NULL; long f_pos = 0; ssize_t size_counter = 0; ssize_t (*decode_function)(FILE*, FILE*, uschar*); -if (mime_stream == NULL) +if (!mime_stream || (f_pos = ftell(mime_stream)) < 0) return FAIL; -f_pos = ftell(mime_stream); - /* build default decode path (will exist since MBOX must be spooled up) */ -(void)string_format(decode_path,1024,"%s/scan/%s",spool_directory,message_id); +decode_path = string_sprintf("%s/scan/%s", spool_directory, message_id); /* try to find 1st option */ -if ((option = string_nextinlist(&list, &sep, - option_buffer, - sizeof(option_buffer))) != NULL) +if ((option = string_nextinlist(&list, &sep, NULL, 0))) { /* parse 1st option */ - if ( (Ustrcmp(option,"false") == 0) || (Ustrcmp(option,"0") == 0) ) + if ((Ustrcmp(option,"false") == 0) || (Ustrcmp(option,"0") == 0)) /* explicitly no decoding */ return FAIL; @@ -358,7 +285,8 @@ decode_function = size_counter = decode_function(mime_stream, decode_file, mime_current_boundary); clearerr(mime_stream); -fseek(mime_stream, f_pos, SEEK_SET); +if (fseek(mime_stream, f_pos, SEEK_SET)) + return DEFER; if (fclose(decode_file) != 0 || size_counter < 0) return DEFER; @@ -369,7 +297,8 @@ mime_content_size = (size_counter + 1023) / 1024; return OK; } -int + +static int mime_get_header(FILE *f, uschar *header) { int c = EOF; @@ -474,21 +403,108 @@ else } +static void +mime_vars_reset(void) +{ +mime_anomaly_level = 0; +mime_anomaly_text = NULL; +mime_boundary = NULL; +mime_charset = NULL; +mime_decoded_filename = NULL; +mime_filename = NULL; +mime_content_description = NULL; +mime_content_disposition = NULL; +mime_content_id = NULL; +mime_content_transfer_encoding = NULL; +mime_content_type = NULL; +mime_is_multipart = 0; +mime_content_size = 0; +} + + +/* Grab a parameter value, dealing with quoting. + +Arguments: + str Input string. Updated on return to point to terminating ; or NUL + +Return: + Allocated string with parameter value +*/ +static uschar * +mime_param_val(uschar ** sp) +{ +uschar * s = *sp; +gstring * val = NULL; + +/* debug_printf_indent(" considering paramval '%s'\n", s); */ + +while (*s && *s != ';') /* ; terminates */ + if (*s == '"') + { + s++; /* skip opening " */ + while (*s && *s != '"') /* " protects ; */ + val = string_catn(val, s++, 1); + if (*s) s++; /* skip closing " */ + } + else + val = string_catn(val, s++, 1); +*sp = s; +return string_from_gstring(val); +} + +static uschar * +mime_next_semicolon(uschar * s) +{ +while (*s && *s != ';') /* ; terminates */ + if (*s == '"') + { + s++; /* skip opening " */ + while (*s && *s != '"') /* " protects ; */ + s++; + if (*s) s++; /* skip closing " */ + } + else + s++; +return s; +} + + +static uschar * +rfc2231_to_2047(const uschar * fname, const uschar * charset, int * len) +{ +gstring * val = string_catn(NULL, US"=?", 2); +uschar c; + +if (charset) + val = string_cat(val, charset); +val = string_catn(val, US"?Q?", 3); + +while ((c = *fname)) + if (c == '%' && isxdigit(fname[1]) && isxdigit(fname[2])) + { + val = string_catn(val, US"=", 1); + val = string_catn(val, ++fname, 2); + fname += 2; + } + else + val = string_catn(val, fname++, 1); + +val = string_catn(val, US"?=", 2); +*len = val->ptr; +return string_from_gstring(val); +} + + int mime_acl_check(uschar *acl, FILE *f, struct mime_boundary_context *context, - uschar **user_msgptr, uschar **log_msgptr) + uschar **user_msgptr, uschar **log_msgptr) { int rc = OK; -uschar *header = NULL; +uschar * header = NULL; struct mime_boundary_context nested_context; /* reserve a line buffer to work in */ -if (!(header = (uschar *)malloc(MIME_MAX_HEADER_SIZE+1))) - { - log_write(0, LOG_PANIC, - "MIME ACL: can't allocate %d bytes of memory.", MIME_MAX_HEADER_SIZE+1); - return DEFER; - } +header = store_get(MIME_MAX_HEADER_SIZE+1); /* Not actually used at the moment, but will be vital to fixing * some RFC 2046 nonconformance later... */ @@ -498,26 +514,12 @@ nested_context.parent = context; while(1) { /* reset all per-part mime variables */ - mime_anomaly_level = 0; - mime_anomaly_text = NULL; - mime_boundary = NULL; - mime_charset = NULL; - mime_decoded_filename = NULL; - mime_filename = NULL; - mime_content_description = NULL; - mime_content_disposition = NULL; - mime_content_id = NULL; - mime_content_transfer_encoding = NULL; - mime_content_type = NULL; - mime_is_multipart = 0; - mime_content_size = 0; - - /* - If boundary is null, we assume that *f is positioned on the start of headers (for example, - at the very beginning of a message. - If a boundary is given, we must first advance to it to reach the start of the next header - block. - */ + mime_vars_reset(); + + /* If boundary is null, we assume that *f is positioned on the start of + headers (for example, at the very beginning of a message. If a boundary is + given, we must first advance to it to reach the start of the next header + block. */ /* NOTE -- there's an error here -- RFC2046 specifically says to * check for outer boundaries. This code doesn't do that, and @@ -526,122 +528,186 @@ while(1) * (I have moved partway towards adding support, however, by adding * a "parent" field to my new boundary-context structure.) */ - if (context != NULL) + if (context) for (;;) { - while(fgets(CS header, MIME_MAX_HEADER_SIZE, f) != NULL) + if (!fgets(CS header, MIME_MAX_HEADER_SIZE, f)) { - /* boundary line must start with 2 dashes */ - if (Ustrncmp(header,"--",2) == 0) - { - if (Ustrncmp((header+2),context->boundary,Ustrlen(context->boundary)) == 0) - { - /* found boundary */ - if (Ustrncmp((header+2+Ustrlen(context->boundary)),"--",2) == 0) - { - /* END boundary found */ - debug_printf("End boundary found %s\n", context->boundary); - return rc; - } - else - debug_printf("Next part with boundary %s\n", context->boundary); + /* Hit EOF or read error. Ugh. */ + DEBUG(D_acl) debug_printf_indent("MIME: Hit EOF ...\n"); + return rc; + } - /* can't use break here */ - goto DECODE_HEADERS; - } + /* boundary line must start with 2 dashes */ + if ( Ustrncmp(header, "--", 2) == 0 + && Ustrncmp(header+2, context->boundary, Ustrlen(context->boundary)) == 0 + ) + { /* found boundary */ + if (Ustrncmp((header+2+Ustrlen(context->boundary)), "--", 2) == 0) + { + /* END boundary found */ + DEBUG(D_acl) debug_printf_indent("MIME: End boundary found %s\n", + context->boundary); + return rc; } + + DEBUG(D_acl) debug_printf_indent("MIME: Next part with boundary %s\n", + context->boundary); + break; } - /* Hit EOF or read error. Ugh. */ - debug_printf("Hit EOF ...\n"); - return rc; } -DECODE_HEADERS: /* parse headers, set up expansion variables */ - while (mime_get_header(f,header)) + while (mime_get_header(f, header)) { - int i; - /* loop through header list */ - for (i = 0; i < mime_header_list_size; i++) + struct mime_header * mh; + + /* look for interesting headers */ + for (mh = mime_header_list; + mh < mime_header_list + mime_header_list_size; + mh++) if (strncmpic(mh->name, header, mh->namelen) == 0) { - uschar *header_value = NULL; - int header_value_len = 0; + uschar * p = header + mh->namelen; + uschar * q; + + /* grab the value (normalize to lower case) + and copy to its corresponding expansion variable */ + + for (q = p; *q != ';' && *q; q++) ; + *mh->value = string_copynlc(p, q-p); + DEBUG(D_acl) debug_printf_indent("MIME: found %s header, value is '%s'\n", + mh->name, *mh->value); + + if (*(p = q)) p++; /* jump past the ; */ - /* found an interesting header? */ - if (strncmpic(mime_header_list[i].name,header,mime_header_list[i].namelen) == 0) { - uschar *p = header + mime_header_list[i].namelen; - /* yes, grab the value (normalize to lower case) - and copy to its corresponding expansion variable */ - while(*p != ';') - { - *p = tolower(*p); - p++; - } - header_value_len = (p - (header + mime_header_list[i].namelen)); - header_value = (uschar *)malloc(header_value_len+1); - memset(header_value,0,header_value_len+1); - p = header + mime_header_list[i].namelen; - Ustrncpy(header_value, p, header_value_len); - debug_printf("Found %s MIME header, value is '%s'\n", mime_header_list[i].name, header_value); - *((uschar **)(mime_header_list[i].value)) = header_value; - - /* make p point to the next character after the closing ';' */ - p += (header_value_len+1); - - /* grab all param=value tags on the remaining line, check if they are interesting */ -NEXT_PARAM_SEARCH: - while (*p != 0) + uschar * mime_fname = NULL; + uschar * mime_fname_rfc2231 = NULL; + uschar * mime_filename_charset = NULL; + BOOL decoding_failed = FALSE; + + /* grab all param=value tags on the remaining line, + check if they are interesting */ + + while (*p) { mime_parameter * mp; - for (mp = mime_parameter_list; - mp < &mime_parameter_list[mime_parameter_list_size]; - mp++) - { - uschar *param_value = NULL; - int param_value_len = 0; - - /* found an interesting parameter? */ - if (strncmpic(mp->name, p, mp->namelen) == 0) + + DEBUG(D_acl) debug_printf_indent("MIME: considering paramlist '%s'\n", p); + + if ( !mime_filename + && strncmpic(CUS"content-disposition:", header, 20) == 0 + && strncmpic(CUS"filename*", p, 9) == 0 + ) + { /* RFC 2231 filename */ + uschar * q; + + /* find value of the filename */ + p += 9; + while(*p != '=' && *p) p++; + if (*p) p++; /* p is filename or NUL */ + q = mime_param_val(&p); /* p now trailing ; or NUL */ + + if (q && *q) { - uschar *q = p + mp->namelen; - int size = 0; - int ptr = 0; + uschar * temp_string, * err_msg; + int slen; - /* yes, grab the value and copy to its corresponding expansion variable */ - while(*q && *q != ';') /* ; terminates */ + /* build up an un-decoded filename over successive + filename*= parameters (for use when 2047 decode fails) */ + + mime_fname_rfc2231 = string_sprintf("%#s%s", + mime_fname_rfc2231, q); + + if (!decoding_failed) { - if (*q == '"') + int size; + if (!mime_filename_charset) { - q++; /* skip leading " */ - while(*q && *q != '"') /* which protects ; */ - param_value = string_cat(param_value, &size, &ptr, q++, 1); - if (*q) q++; /* skip trailing " */ + uschar * s = q; + + /* look for a ' in the "filename" */ + while(*s != '\'' && *s) s++; /* s is 1st ' or NUL */ + + if ((size = s-q) > 0) + mime_filename_charset = string_copyn(q, size); + + if (*(p = s)) p++; + while(*p == '\'') p++; /* p is after 2nd ' */ } else - param_value = string_cat(param_value, &size, &ptr, q++, 1); + p = q; + + DEBUG(D_acl) debug_printf_indent("MIME: charset %s fname '%s'\n", + mime_filename_charset ? mime_filename_charset : US"", p); + + temp_string = rfc2231_to_2047(p, mime_filename_charset, &slen); + DEBUG(D_acl) debug_printf_indent("MIME: 2047-name %s\n", temp_string); + + temp_string = rfc2047_decode(temp_string, FALSE, NULL, ' ', + NULL, &err_msg); + DEBUG(D_acl) debug_printf_indent("MIME: plain-name %s\n", temp_string); + + if (!temp_string || (size = Ustrlen(temp_string)) == slen) + decoding_failed = TRUE; + else + /* build up a decoded filename over successive + filename*= parameters */ + + mime_filename = mime_fname = mime_fname + ? string_sprintf("%s%s", mime_fname, temp_string) + : temp_string; } - param_value[ptr++] = '\0'; - param_value_len = ptr; - - param_value = rfc2047_decode(param_value, check_rfc2047_length, NULL, 32, ¶m_value_len, &q); - debug_printf("Found %s MIME parameter in %s header, value is '%s'\n", mp->name, mime_header_list[i].name, param_value); - *((uschar **)(mp->value)) = param_value; - p += (mp->namelen + param_value_len + 1); - goto NEXT_PARAM_SEARCH; + } } - } + + else + /* look for interesting parameters */ + for (mp = mime_parameter_list; + mp < mime_parameter_list + nelem(mime_parameter_list); + mp++ + ) if (strncmpic(mp->name, p, mp->namelen) == 0) + { + uschar * q; + uschar * dummy_errstr; + + /* grab the value and copy to its expansion variable */ + p += mp->namelen; + q = mime_param_val(&p); /* p now trailing ; or NUL */ + + *mp->value = q && *q + ? rfc2047_decode(q, check_rfc2047_length, NULL, 32, NULL, + &dummy_errstr) + : NULL; + DEBUG(D_acl) debug_printf_indent( + "MIME: found %s parameter in %s header, value '%s'\n", + mp->name, mh->name, *mp->value); + + break; /* done matching param names */ + } + + /* There is something, but not one of our interesting parameters. - Advance to the next semicolon */ - while(*p != ';') p++; - p++; + Advance past the next semicolon */ + p = mime_next_semicolon(p); + if (*p) p++; + } /* param scan on line */ + + if (strncmpic(CUS"content-disposition:", header, 20) == 0) + { + if (decoding_failed) mime_filename = mime_fname_rfc2231; + + DEBUG(D_acl) debug_printf_indent( + "MIME: found %s parameter in %s header, value is '%s'\n", + "filename", mh->name, mime_filename); + } } } } - } /* set additional flag variables (easier access) */ - if ( (mime_content_type != NULL) && - (Ustrncmp(mime_content_type,"multipart",9) == 0) ) + if ( mime_content_type + && Ustrncmp(mime_content_type,"multipart",9) == 0 + ) mime_is_multipart = 1; /* Make a copy of the boundary pointer. @@ -672,7 +738,9 @@ NEXT_PARAM_SEARCH: (nested_context.boundary != NULL) && (Ustrncmp(mime_content_type,"multipart",9) == 0) ) { - debug_printf("Entering multipart recursion, boundary '%s'\n", nested_context.boundary); + DEBUG(D_acl) + debug_printf_indent("MIME: Entering multipart recursion, boundary '%s'\n", + nested_context.boundary); nested_context.context = context && context->context == MBC_ATTACHMENT @@ -688,7 +756,7 @@ NEXT_PARAM_SEARCH: else if ( (mime_content_type != NULL) && (Ustrncmp(mime_content_type,"message/rfc822",14) == 0) ) { - uschar *rfc822name = NULL; + const uschar *rfc822name = NULL; uschar filename[2048]; int file_nr = 0; int result = 0; @@ -717,20 +785,23 @@ NEXT_PARAM_SEARCH: if (!mime_decoded_filename) /* decoding failed */ { log_write(0, LOG_MAIN, - "mime_regex acl condition warning - could not decode RFC822 MIME part to file."); - return DEFER; + "MIME acl condition warning - could not decode RFC822 MIME part to file."); + rc = DEFER; + goto out; } mime_decoded_filename = NULL; } NO_RFC822: /* If the boundary of this instance is NULL, we are finished here */ - if (context == NULL) break; + if (!context) break; if (context->context == MBC_COVERLETTER_ONESHOT) context->context = MBC_ATTACHMENT; } +out: +mime_vars_reset(); return rc; }