* 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. */
*/
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
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;
Returns: a string
*/
-uschar *
+const uschar *
transport_rcpt_address(address_item *addr, BOOL include_affixes)
{
uschar *at;
*/
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;
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 */
}
/* 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,
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)
{
{
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);
}
}
}
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");
}
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;
)
{
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 */
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)
/* 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. */
? !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. */
*/
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", message_id, tpname);
+ return;
+ }
+
DEBUG(D_transport) debug_printf("updating wait-%s database\n", tpname);
/* Open the database for this transport */
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];
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. */
current continue sequence is greater than the maximum supplied as an argument,
or greater than the global connection_max_messages, which, if set, overrides.
+It is also called if conditions are otherwise right for pipelining a QUIT after
+the message data, since if there is another message waiting we do not want to
+send that QUIT.
+
Arguments:
transport_name name of the transport
hostname name of the host
} 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;
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),
/* 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;
#ifndef DISABLE_TLS
if (smtp_peer_options & OPTION_TLS) i += 6;
#endif
-#ifdef EXPERIMENTAL_ESMTP_LIMITS
+#ifndef DISABLE_ESMTP_LIMITS
if (continue_limit_mail || continue_limit_rcpt || continue_limit_rcptdom)
i += 4;
#endif
argv[i++] = US"-MCT";
#endif
-#ifdef EXPERIMENTAL_ESMTP_LIMITS
+#ifndef DISABLE_ESMTP_LIMITS
if (continue_limit_rcpt || continue_limit_rcptdom)
{
argv[i++] = US"-MCL";
BOOL
transport_pass_socket(const uschar *transport_name, const uschar *hostname,
const uschar *hostaddress, uschar *id, int socket_fd
-#ifdef EXPERIMENTAL_ESMTP_LIMITS
+#ifndef DISABLE_ESMTP_LIMITS
, unsigned peer_limit_mail, unsigned peer_limit_rcpt, unsigned peer_limit_rcptdom
#endif
)
DEBUG(D_transport) debug_printf("transport_pass_socket entered\n");
-#ifdef EXPERIMENTAL_ESMTP_LIMITS
+#ifndef DISABLE_ESMTP_LIMITS
continue_limit_mail = peer_limit_mail;
continue_limit_rcpt = peer_limit_rcpt;
continue_limit_rcptdom = peer_limit_rcptdom;
+/* 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
BOOL
transport_set_up_command(const uschar *** argvptr, const uschar * cmd,
- BOOL expand_arguments, int expand_failed, address_item *addr,
+ unsigned flags, int expand_failed, address_item * addr,
const uschar * etext, uschar ** errptr)
{
const uschar ** argv, * s;
debug_printf(" argv[%d] = '%s'\n", i, string_printing(argv[i]));
}
-if (expand_arguments)
+if (flags & TSUC_EXPAND_ARGS)
{
- BOOL allow_dollar_recipients = addr && addr->parent
- && 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]; i++)
{
+ DEBUG(D_expand) debug_printf_indent("arg %d\n", i);
+
/* Handle special fudge for passing an address list */
if (addr &&
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++;
}
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)
{
for (int address_pipe_i = 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--;
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)
{
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;
}
}
{
debug_printf("direct command after expansion:\n");
for (int i = 0; argv[i]; i++)
- debug_printf(" argv[%d] = %s\n", i, string_printing(argv[i]));
+ {
+ debug_printf(" argv[%d] = '%s'\n", i, string_printing(argv[i]));
+ debug_print_taint(argv[i]);
+ }
}
}