X-Git-Url: https://git.exim.org/exim.git/blobdiff_plain/4191cb150300d310ab5fa22ce2cfb02b6f6051b0..fa1c8faf169384bebaa8d172f491574c45ae2aa4:/src/src/transport.c diff --git a/src/src/transport.c b/src/src/transport.c index 96a936503..a0f097579 100644 --- a/src/src/transport.c +++ b/src/src/transport.c @@ -2,9 +2,10 @@ * Exim - an Internet mail transport agent * *************************************************/ +/* Copyright (c) The Exim Maintainers 2020 - 2023 */ /* Copyright (c) University of Cambridge 1995 - 2018 */ -/* Copyright (c) The Exim Maintainers 2020 - 2021 */ /* See the file NOTICE for conditions of use and distribution. */ +/* SPDX-License-Identifier: GPL-2.0-or-later */ /* General functions concerned with transportation, and generic options for all transports. */ @@ -433,10 +434,10 @@ Returns: TRUE on success, FALSE on failure (with errno preserved) */ BOOL -write_chunk(transport_ctx * tctx, uschar *chunk, int len) +write_chunk(transport_ctx * tctx, const uschar * chunk, int len) { -uschar *start = chunk; -uschar *end = chunk + len; +const uschar * start = chunk; +const uschar * end = chunk + len; int mlen = DELIVER_OUT_BUFFER_SIZE - nl_escape_length - 2; /* The assumption is made that the check string will never stretch over move @@ -473,7 +474,7 @@ if (nl_partial_match >= 0) for possible escaping. The code for the non-NL route should be as fast as possible. */ -for (uschar * ptr = start; ptr < end; ptr++) +for (const uschar * ptr = start; ptr < end; ptr++) { int ch, len; @@ -579,7 +580,7 @@ Arguments: Returns: a string */ -uschar * +const uschar * transport_rcpt_address(address_item *addr, BOOL include_affixes) { uschar *at; @@ -703,9 +704,9 @@ Returns: TRUE on success; FALSE on failure. */ BOOL transport_headers_send(transport_ctx * tctx, - BOOL (*sendfn)(transport_ctx * tctx, uschar * s, int len)) + BOOL (*sendfn)(transport_ctx * tctx, const uschar * s, int len)) { -const uschar *list; +const uschar * list; transport_instance * tblock = tctx ? tctx->tblock : NULL; address_item * addr = tctx ? tctx->addr : NULL; @@ -760,15 +761,18 @@ for (header_line * h = header_list; h; h = h->next) if (h->type != htype_old) if (include_header) { + int len; if (tblock && tblock->rewrite_rules) { rmark reset_point = store_mark(); - header_line *hh; + header_line * hh; if ((hh = rewrite_header(h, NULL, NULL, tblock->rewrite_rules, tblock->rewrite_existflags, FALSE))) { - if (!sendfn(tctx, hh->text, hh->slen)) return FALSE; + len = hh->slen; + if (tctx->options & topt_truncate_headers && len > 998) len = 998; + if (!sendfn(tctx, hh->text, len)) return FALSE; store_reset(reset_point); continue; /* With the next header line */ } @@ -776,13 +780,15 @@ for (header_line * h = header_list; h; h = h->next) if (h->type != htype_old) /* Either no rewriting rules, or it didn't get rewritten */ - if (!sendfn(tctx, h->text, h->slen)) return FALSE; + len = h->slen; + if (tctx->options & topt_truncate_headers && len > 998) len = 998; + if (!sendfn(tctx, h->text, len)) return FALSE; } /* Header removed */ else - DEBUG(D_transport) debug_printf("removed header line:\n%s---\n", h->text); + DEBUG(D_transport) debug_printf("removed header line:\n %s---\n", h->text); } /* Add on any address-specific headers. If there are multiple addresses, @@ -798,8 +804,8 @@ Headers added to an address by a router are guaranteed to end with a newline. if (addr) { - header_line *hprev = addr->prop.extra_headers; - header_line *hnext, * h; + header_line * hprev = addr->prop.extra_headers, * hnext, * h; + for (int i = 0; i < 2; i++) for (h = hprev, hprev = NULL; h; h = hnext) { @@ -810,7 +816,7 @@ if (addr) { if (!sendfn(tctx, h->text, h->slen)) return FALSE; DEBUG(D_transport) - debug_printf("added header line(s):\n%s---\n", h->text); + debug_printf("added header line(s):\n %s---\n", h->text); } } } @@ -838,7 +844,7 @@ if (tblock && (list = CUS tblock->add_headers)) return FALSE; DEBUG(D_transport) { - debug_printf("added header line:\n%s", s); + debug_printf("added header line:\n %s", s); if (s[len-1] != '\n') debug_printf("\n"); debug_printf("---\n"); } @@ -1037,7 +1043,7 @@ if (tctx->options & topt_use_bdat) if (!(tctx->options & topt_no_body)) { if ((fsize = lseek(deliver_datafile, 0, SEEK_END)) < 0) return FALSE; - fsize -= SPOOL_DATA_START_OFFSET; + fsize -= spool_data_start_offset(message_id); if (size_limit > 0 && fsize > size_limit) fsize = size_limit; size = hsize + fsize; @@ -1095,7 +1101,7 @@ if ( f.spool_file_wireformat ) { ssize_t copied = 0; - off_t offset = SPOOL_DATA_START_OFFSET; + off_t offset = spool_data_start_offset(message_id); /* Write out any header data in the buffer */ @@ -1133,7 +1139,7 @@ if (!(tctx->options & topt_no_body)) nl_check_length = abs(nl_check_length); nl_partial_match = 0; - if (lseek(deliver_datafile, SPOOL_DATA_START_OFFSET, SEEK_SET) < 0) + if (lseek(deliver_datafile, spool_data_start_offset(message_id), SEEK_SET) < 0) return FALSE; while ( (len = MIN(DELIVER_IN_BUFFER_SIZE, size)) > 0 && (len = read(deliver_datafile, deliver_in_buffer, len)) > 0) @@ -1155,8 +1161,12 @@ f.spool_file_wireformat = FALSE; /* If requested, add a terminating "." line (SMTP output). */ -if (tctx->options & topt_end_dot && !write_chunk(tctx, US".\n", 2)) - return FALSE; +if (tctx->options & topt_end_dot) + { + smtp_debug_cmd(US".", 0); + if (!write_chunk(tctx, US".\n", 2)) + return FALSE; + } /* Write out any remaining data in the buffer before returning. */ @@ -1426,7 +1436,7 @@ if (yield) ? !write_chunk(tctx, US".\n", 2) : !write_chunk(tctx, US"\n.\n", 3) ) ) - yield = FALSE; + { smtp_debug_cmd(US".", 0); yield = FALSE; } /* Write out any remaining data in the buffer. */ @@ -1487,12 +1497,19 @@ Returns: nothing */ void -transport_update_waiting(host_item *hostlist, uschar *tpname) +transport_update_waiting(host_item * hostlist, uschar * tpname) { const uschar *prevname = US""; open_db dbblock; open_db *dbm_file; +if (!is_new_message_id(message_id)) + { + DEBUG(D_transport) debug_printf("message_id %s is not new format; " + "skipping wait-%s database update\n", tpname); + return; + } + DEBUG(D_transport) debug_printf("updating wait-%s database\n", tpname); /* Open the database for this transport */ @@ -1507,7 +1524,7 @@ that the message id is in each host record. */ for (host_item * host = hostlist; host; host = host->next) { BOOL already = FALSE; - dbdata_wait *host_record; + dbdata_wait * host_record; int host_length; uschar buffer[256]; @@ -1533,8 +1550,27 @@ for (host_item * host = hostlist; host; host = host->next) for (uschar * s = host_record->text; s < host_record->text + host_length; s += MESSAGE_ID_LENGTH) + { + /* If any ID is seen which is not new-format, wipe the record and + any continuations */ + + if (!is_new_message_id(s)) + { + DEBUG(D_hints_lookup) + debug_printf_indent("NOTE: old or corrupt message-id found in wait=%.200s" + " hints DB; deleting records for %s\n", tpname, host->name); + + (void) dbfn_delete(dbm_file, host->name); + for (int i = host_record->sequence - 1; i >= 0; i--) + (void) dbfn_delete(dbm_file, + (sprintf(CS buffer, "%.200s:%d", host->name, i), buffer)); + + host_record->count = host_record->sequence = 0; + break; + } if (Ustrncmp(s, message_id, MESSAGE_ID_LENGTH) == 0) { already = TRUE; break; } + } /* If we haven't found this message in the main record, search any continuation records that exist. */ @@ -1642,13 +1678,14 @@ typedef struct msgq_s } msgq_t; BOOL -transport_check_waiting(const uschar *transport_name, const uschar *hostname, - int local_message_max, uschar *new_message_id, oicf oicf_func, void *oicf_data) +transport_check_waiting(const uschar * transport_name, const uschar * hostname, + int local_message_max, uschar * new_message_id, + oicf oicf_func, void * oicf_data) { -dbdata_wait *host_record; +dbdata_wait * host_record; int host_length; open_db dbblock; -open_db *dbm_file; +open_db * dbm_file; int i; struct stat statbuf; @@ -1725,6 +1762,22 @@ while (1) for (i = 0; i < host_record->count; ++i) { + /* If any ID is seen which is not new-format, wipe the record and + any continuations */ + + if (!is_new_message_id(host_record->text + (i * MESSAGE_ID_LENGTH))) + { + uschar buffer[256]; + DEBUG(D_hints_lookup) + debug_printf_indent("NOTE: old or corrupt message-id found in wait=%.200s" + " hints DB; deleting records for %s\n", transport_name, hostname); + (void) dbfn_delete(dbm_file, hostname); + for (int i = host_record->sequence - 1; i >= 0; i--) + (void) dbfn_delete(dbm_file, + (sprintf(CS buffer, "%.200s:%d", hostname, i), buffer)); + dbfn_close(dbm_file); + goto retfalse; + } msgq[i].bKeep = TRUE; Ustrncpy_nt(msgq[i].message_id, host_record->text + (i * MESSAGE_ID_LENGTH), @@ -1875,8 +1928,8 @@ return FALSE; /* Just the regain-root-privilege exec portion */ void -transport_do_pass_socket(const uschar *transport_name, const uschar *hostname, - const uschar *hostaddress, uschar *id, int socket_fd) +transport_do_pass_socket(const uschar * transport_name, const uschar * hostname, + const uschar * hostaddress, uschar * id, int socket_fd) { int i = 13; const uschar **argv; @@ -1972,6 +2025,7 @@ if (socket_fd != 0) DEBUG(D_exec) debug_print_argv(argv); exim_nullstd(); /* Ensure std{out,err} exist */ +/* argv[0] should be untainted, from child_exec_exim() */ execv(CS argv[0], (char *const *)argv); DEBUG(D_any) debug_printf("execv failed: %s\n", strerror(errno)); @@ -2048,20 +2102,46 @@ else +/* Enforce all args untainted, for consistency with a router-sourced pipe +command, where (because the whole line is passed as one to the tpt) a +tainted arg taints the executable name. It's unclear also that letting an +attacker supply command arguments is wise. */ + +static BOOL +arg_is_tainted(const uschar * s, int argn, address_item * addr, + const uschar * etext, uschar ** errptr) +{ +if (is_tainted(s)) + { + uschar * msg = string_sprintf("Tainted arg %d for %s command: '%s'", + argn, etext, s); + if (addr) + { + addr->transport_return = FAIL; + addr->message = msg; + } + else *errptr = msg; + return TRUE; + } +return FALSE; +} + + /************************************************* * Set up direct (non-shell) command * *************************************************/ /* This function is called when a command line is to be parsed and executed directly, without the use of /bin/sh. It is called by the pipe transport, -the queryprogram router, and also from the main delivery code when setting up a +the queryprogram router, for any ${run } expansion, +and also from the main delivery code when setting up a transport filter process. The code for ETRN also makes use of this; in that case, no addresses are passed. Arguments: argvptr pointer to anchor for argv vector cmd points to the command string (modified IN PLACE) - expand_arguments true if expansion is to occur + flags bits for expand-args, allow taint, allow $recipients expand_failed error value to set if expansion fails; not relevant if addr == NULL addr chain of addresses, or NULL @@ -2074,15 +2154,12 @@ Returns: TRUE if all went well; otherwise an error will be */ BOOL -transport_set_up_command(const uschar ***argvptr, uschar *cmd, - BOOL expand_arguments, int expand_failed, address_item *addr, - uschar *etext, uschar **errptr) +transport_set_up_command(const uschar *** argvptr, const uschar * cmd, + unsigned flags, int expand_failed, address_item * addr, + const uschar * etext, uschar ** errptr) { -const uschar **argv; -uschar *s, *ss; -int address_count = 0; -int argcount = 0; -int max_args; +const uschar ** argv, * s; +int address_count = 0, argcount = 0, max_args; /* Get store in which to build an argument list. Count the number of addresses supplied, and allow for that many arguments, plus an additional 60, which @@ -2099,33 +2176,30 @@ trailing space at the start and end. Double-quoted arguments can contain \\ and arguments are verbatim. Copy each argument into a new string. */ s = cmd; -while (isspace(*s)) s++; +Uskip_whitespace(&s); -for (; *s != 0 && argcount < max_args; argcount++) +for (; *s && argcount < max_args; argcount++) { if (*s == '\'') { - ss = s + 1; - while (*ss != 0 && *ss != '\'') ss++; - argv[argcount] = ss = store_get(ss - s++, cmd); - while (*s != 0 && *s != '\'') *ss++ = *s++; - if (*s != 0) s++; - *ss++ = 0; + int n = Ustrcspn(++s, "'"); + argv[argcount] = string_copyn(s, n); + if (*(s += n) == '\'') s++; } else argv[argcount] = string_dequote(CUSS &s); - while (isspace(*s)) s++; + Uskip_whitespace(&s); } -argv[argcount] = US 0; +argv[argcount] = NULL; /* If *s != 0 we have run out of argument slots. */ -if (*s != 0) +if (*s) { uschar *msg = string_sprintf("Too many arguments in command \"%s\" in " "%s", cmd, etext); - if (addr != NULL) + if (addr) { addr->transport_return = FAIL; addr->message = msg; @@ -2157,18 +2231,18 @@ DEBUG(D_transport) debug_printf(" argv[%d] = '%s'\n", i, string_printing(argv[i])); } -if (expand_arguments) +if (flags & TSUC_EXPAND_ARGS) { - BOOL allow_dollar_recipients = addr != NULL && - addr->parent != NULL && - Ustrcmp(addr->parent->address, "system-filter") == 0; + BOOL allow_dollar_recipients = (flags & TSUC_ALLOW_RECIPIENTS) + || (addr && addr->parent && Ustrcmp(addr->parent->address, "system-filter") == 0); /*XXX could we check this at caller? */ - for (int i = 0; argv[i] != US 0; i++) + for (int i = 0; argv[i]; i++) { + DEBUG(D_expand) debug_printf_indent("arg %d\n", i); /* Handle special fudge for passing an address list */ - if (addr != NULL && + if (addr && (Ustrcmp(argv[i], "$pipe_addresses") == 0 || Ustrcmp(argv[i], "${pipe_addresses}") == 0)) { @@ -2189,6 +2263,16 @@ if (expand_arguments) for (address_item * ad = addr; ad; ad = ad->next) { + /* $pipe_addresses is spefically not checked for taint, because there is + a testcase (321) depending on it. It's unclear if the exact thing being + done really needs to be legitimate, though I suspect it reflects an + actual use-case that showed up a bug. + This is a hole in the taint-pretection, mitigated only in that + shell-syntax metachars cannot be injected via this route. */ + + DEBUG(D_transport) if (is_tainted(ad->address)) + debug_printf("tainted element '%s' from $pipe_addresses\n", ad->address); + argv[i++] = ad->address; argcount++; } @@ -2200,7 +2284,7 @@ if (expand_arguments) /* Handle special case of $address_pipe when af_force_command is set */ - else if (addr != NULL && testflag(addr,af_force_command) && + else if (addr && testflag(addr,af_force_command) && (Ustrcmp(argv[i], "$address_pipe") == 0 || Ustrcmp(argv[i], "${address_pipe}") == 0)) { @@ -2218,9 +2302,9 @@ if (expand_arguments) address_pipe_argv = store_get((address_pipe_max_args+1)*sizeof(uschar *), GET_UNTAINTED); /* +1 because addr->local_part[0] == '|' since af_force_command is set */ - s = expand_string(addr->local_part + 1); + s = expand_cstring(addr->local_part + 1); - if (!s || *s == '\0') + if (!s || !*s) { addr->transport_return = FAIL; addr->message = string_sprintf("Expansion of \"%s\" " @@ -2229,33 +2313,29 @@ if (expand_arguments) return FALSE; } - while (isspace(*s)) s++; /* strip leading space */ + Uskip_whitespace(&s); /* strip leading space */ while (*s && address_pipe_argcount < address_pipe_max_args) { if (*s == '\'') - { - int n; - for (ss = s + 1; *ss && *ss != '\''; ) ss++; - n = ss - s++; - address_pipe_argv[address_pipe_argcount++] = ss = store_get(n, s); - while (*s && *s != '\'') *ss++ = *s++; - if (*s) s++; - *ss++ = 0; - } - else address_pipe_argv[address_pipe_argcount++] = - string_copy(string_dequote(CUSS &s)); - while (isspace(*s)) s++; /* strip space after arg */ + { + int n = Ustrcspn(++s, "'"); + argv[argcount] = string_copyn(s, n); + if (*(s += n) == '\'') s++; + } + else + address_pipe_argv[address_pipe_argcount++] = string_dequote(CUSS &s); + Uskip_whitespace(&s); /* strip space after arg */ } - address_pipe_argv[address_pipe_argcount] = US 0; + address_pipe_argv[address_pipe_argcount] = NULL; /* If *s != 0 we have run out of argument slots. */ - if (*s != 0) + if (*s) { uschar *msg = string_sprintf("Too many arguments in $address_pipe " "\"%s\" in %s", addr->local_part + 1, etext); - if (addr != NULL) + if (addr) { addr->transport_return = FAIL; addr->message = msg; @@ -2265,8 +2345,9 @@ if (expand_arguments) } /* address_pipe_argcount - 1 - * because we are replacing $address_pipe in the argument list - * with the first thing it expands to */ + because we are replacing $address_pipe in the argument list + with the first thing it expands to */ + if (argcount + address_pipe_argcount - 1 > max_args) { addr->transport_return = FAIL; @@ -2296,9 +2377,13 @@ if (expand_arguments) [argv 0][argv 1][argv 2=pipeargv[0]][argv 3=pipeargv[1]][old argv 3][0] */ for (int address_pipe_i = 0; - address_pipe_argv[address_pipe_i] != US 0; + address_pipe_argv[address_pipe_i]; address_pipe_i++, argcount++) - argv[i++] = address_pipe_argv[address_pipe_i]; + { + uschar * s = address_pipe_argv[address_pipe_i]; + if (arg_is_tainted(s, i, addr, etext, errptr)) return FALSE; + argv[i++] = s; + } /* Subtract one since we replace $address_pipe */ argcount--; @@ -2310,9 +2395,10 @@ if (expand_arguments) else { const uschar *expanded_arg; + BOOL enable_dollar_recipients_g = f.enable_dollar_recipients; f.enable_dollar_recipients = allow_dollar_recipients; expanded_arg = expand_cstring(argv[i]); - f.enable_dollar_recipients = FALSE; + f.enable_dollar_recipients = enable_dollar_recipients_g; if (!expanded_arg) { @@ -2327,6 +2413,17 @@ if (expand_arguments) else *errptr = msg; return FALSE; } + + if ( f.running_in_test_harness && is_tainted(expanded_arg) + && Ustrcmp(etext, "queryprogram router") == 0) + { /* hack, would be good to not need it */ + DEBUG(D_transport) + debug_printf("SPECIFIC TESTSUITE EXEMPTION: tainted arg '%s'\n", + expanded_arg); + } + else if ( !(flags & TSUC_ALLOW_TAINTED_ARGS) + && arg_is_tainted(expanded_arg, i, addr, etext, errptr)) + return FALSE; argv[i] = expanded_arg; } } @@ -2334,8 +2431,11 @@ if (expand_arguments) DEBUG(D_transport) { debug_printf("direct command after expansion:\n"); - for (int i = 0; argv[i] != US 0; i++) - debug_printf(" argv[%d] = %s\n", i, string_printing(argv[i])); + for (int i = 0; argv[i]; i++) + { + debug_printf(" argv[%d] = '%s'\n", i, string_printing(argv[i])); + debug_print_taint(argv[i]); + } } }