Fix ${run } arg parsing
[exim.git] / src / src / transport.c
index a04663277acc971bbe6dd6813e4bc09e909e9ebd..d6cedf91171eea5a1f93ecd880a9c6ee9ba912cb 100644 (file)
@@ -2,9 +2,10 @@
 *     Exim - an Internet mail transport agent    *
 *************************************************/
 
+/* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
-/* Copyright (c) The Exim Maintainers 2020 */
 /* 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. */
@@ -253,7 +254,6 @@ for (int i = 0; i < 100; i++)
 
   for(;;)
     {
-    fd_set fds;
     /* This code makes use of alarm() in order to implement the timeout. This
     isn't a very tidy way of doing things. Using non-blocking I/O with select()
     provides a neater approach. However, I don't know how to do this when TLS is
@@ -281,8 +281,7 @@ for (int i = 0; i < 100; i++)
     if (rc >= 0 || errno != ENOTCONN || connretry <= 0)
       break;
 
-    FD_ZERO(&fds); FD_SET(fd, &fds);
-    select(fd+1, NULL, &fds, NULL, NULL);      /* could set timout? */
+    poll_one_fd(fd, POLLOUT, -1);              /* could set timeout? retval check? */
     connretry--;
     }
 
@@ -653,7 +652,7 @@ so that we don't handle it again. */
 
 for (ppp = *pdlist; ppp; ppp = ppp->next) if (p == ppp->ptr) return TRUE;
 
-ppp = store_get(sizeof(struct aci), FALSE);
+ppp = store_get(sizeof(struct aci), GET_UNTAINTED);
 ppp->next = *pdlist;
 *pdlist = ppp;
 ppp->ptr = p;
@@ -677,7 +676,7 @@ if (ppp) return TRUE;
 
 /* Remember what we have output, and output it. */
 
-ppp = store_get(sizeof(struct aci), FALSE);
+ppp = store_get(sizeof(struct aci), GET_UNTAINTED);
 ppp->next = *pplist;
 *pplist = ppp;
 ppp->ptr = pp;
@@ -784,7 +783,7 @@ for (header_line * h = header_list; h; h = h->next) if (h->type != htype_old)
   /* 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,
@@ -800,8 +799,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)
       {
@@ -812,7 +811,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);
        }
       }
   }
@@ -840,7 +839,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");
          }
@@ -886,7 +885,7 @@ transport_write_timeout non-zero.
 
 Arguments:
   tctx
-    (fd, msg)          Either and fd, to write the message to,
+    (fd, msg)          Either an fd, to write the message to,
                        or a string: if null write message to allocated space
                        otherwire take content as headers.
     addr                (chain of) addresses (for extra headers), or NULL;
@@ -905,6 +904,7 @@ Arguments:
       add_delivery_date     if TRUE, add a "delivery-date" header
       use_crlf              if TRUE, turn NL into CR LF
       end_dot               if TRUE, send a terminating "." line at the end
+      no_flush             if TRUE, do not flush at end
       no_headers            if TRUE, omit the headers
       no_body               if TRUE, omit the body
     check_string          a string to check for at the start of lines, or NULL
@@ -958,10 +958,10 @@ if (!(tctx->options & topt_no_headers))
 
   if (tctx->options & topt_add_return_path)
     {
-    uschar buffer[ADDRESS_MAXLENGTH + 20];
-    int n = sprintf(CS buffer, "Return-path: <%.*s>\n", ADDRESS_MAXLENGTH,
-      return_path);
-    if (!write_chunk(tctx, buffer, n)) goto bad;
+    int n;
+    uschar * s = string_sprintf("Return-path: <%.*s>\n%n",
+                          EXIM_EMAILADDR_MAX, return_path, &n);
+    if (!write_chunk(tctx, s, n)) goto bad;
     }
 
   /* Add envelope-to: if requested */
@@ -1156,13 +1156,18 @@ 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. */
 
-return (len = chunk_ptr - deliver_out_buffer) <= 0 ||
-  transport_write_block(tctx, deliver_out_buffer, len, FALSE);
+return (len = chunk_ptr - deliver_out_buffer) <= 0
+  || transport_write_block(tctx, deliver_out_buffer, len,
+                           !!(tctx->options & topt_no_flush));
 }
 
 
@@ -1260,7 +1265,7 @@ if ((write_pid = exim_fork(US"tpt-filter-writer")) == 0)
 
   tctx->u.fd = fd_write;
   tctx->check_string = tctx->escape_string = NULL;
-  tctx->options &= ~(topt_use_crlf | topt_end_dot | topt_use_bdat);
+  tctx->options &= ~(topt_use_crlf | topt_end_dot | topt_use_bdat | topt_no_flush);
 
   rc = internal_transport_write_message(tctx, size_limit);
 
@@ -1396,7 +1401,7 @@ if (write_pid > 0)
         yield = FALSE;
        }
       else if (!ok)
-        {
+        {              /* Try to drain the pipe; read fails are don't care */
        int dummy = read(pfd[pipe_read], (void *)&save_errno, sizeof(int));
         dummy = read(pfd[pipe_read], (void *)&tctx->addr->more_errno, sizeof(int));
         dummy = read(pfd[pipe_read], (void *)&tctx->addr->delivery_time, sizeof(struct timeval));
@@ -1426,7 +1431,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. */
 
@@ -1521,7 +1526,7 @@ for (host_item * host = hostlist; host; host = host->next)
 
   if (!(host_record = dbfn_read(dbm_file, host->name)))
     {
-    host_record = store_get(sizeof(dbdata_wait) + MESSAGE_ID_LENGTH, FALSE);
+    host_record = store_get(sizeof(dbdata_wait) + MESSAGE_ID_LENGTH, GET_UNTAINTED);
     host_record->count = host_record->sequence = 0;
     }
 
@@ -1585,7 +1590,7 @@ for (host_item * host = hostlist; host; host = host->next)
   else
     {
     dbdata_wait *newr =
-      store_get(sizeof(dbdata_wait) + host_length + MESSAGE_ID_LENGTH, FALSE);
+      store_get(sizeof(dbdata_wait) + host_length + MESSAGE_ID_LENGTH, GET_UNTAINTED);
     memcpy(newr, host_record, sizeof(dbdata_wait) + host_length);
     host_record = newr;
     }
@@ -1599,7 +1604,8 @@ for (host_item * host = hostlist; host; host = host->next)
   /* Update the database */
 
   dbfn_write(dbm_file, host->name, host_record, sizeof(dbdata_wait) + host_length);
-  DEBUG(D_transport) debug_printf("added to list for %s\n", host->name);
+  DEBUG(D_transport) debug_printf("added %.*s to queue for %s\n",
+                                 MESSAGE_ID_LENGTH, message_id, host->name);
   }
 
 /* All now done */
@@ -1657,6 +1663,7 @@ DEBUG(D_transport)
   debug_printf("transport_check_waiting entered\n");
   debug_printf("  sequence=%d local_max=%d global_max=%d\n",
     continue_sequence, local_message_max, connection_max_messages);
+  acl_level++;
   }
 
 /* Do nothing if we have hit the maximum number that can be send down one
@@ -1666,23 +1673,23 @@ if (connection_max_messages >= 0) local_message_max = connection_max_messages;
 if (local_message_max > 0 && continue_sequence >= local_message_max)
   {
   DEBUG(D_transport)
-    debug_printf("max messages for one connection reached: returning\n");
-  return FALSE;
+    debug_printf_indent("max messages for one connection reached: returning\n");
+  goto retfalse;
   }
 
 /* Open the waiting information database. */
 
 if (!(dbm_file = dbfn_open(string_sprintf("wait-%.200s", transport_name),
                          O_RDWR, &dbblock, TRUE, TRUE)))
-  return FALSE;
+  goto retfalse;
 
 /* See if there is a record for this host; if not, there's nothing to do. */
 
 if (!(host_record = dbfn_read(dbm_file, hostname)))
   {
   dbfn_close(dbm_file);
-  DEBUG(D_transport) debug_printf("no messages waiting for %s\n", hostname);
-  return FALSE;
+  DEBUG(D_transport) debug_printf_indent("no messages waiting for %s\n", hostname);
+  goto retfalse;
   }
 
 /* If the data in the record looks corrupt, just log something and
@@ -1693,7 +1700,7 @@ if (host_record->count > WAIT_NAME_MAX)
   dbfn_close(dbm_file);
   log_write(0, LOG_MAIN|LOG_PANIC, "smtp-wait database entry for %s has bad "
     "count=%d (max=%d)", hostname, host_record->count, WAIT_NAME_MAX);
-  return FALSE;
+  goto retfalse;
   }
 
 /* Scan the message ids in the record from the end towards the beginning,
@@ -1717,7 +1724,7 @@ while (1)
 
   /* create an array to read entire message queue into memory for processing  */
 
-  msgq = store_get(sizeof(msgq_t) * host_record->count, FALSE);
+  msgq = store_get(sizeof(msgq_t) * host_record->count, GET_UNTAINTED);
   msgq_count = host_record->count;
   msgq_actual = msgq_count;
 
@@ -1725,7 +1732,7 @@ while (1)
     {
     msgq[i].bKeep = TRUE;
 
-    Ustrncpy_nt(msgq[i].message_id, host_record->text + (i * MESSAGE_ID_LENGTH), 
+    Ustrncpy_nt(msgq[i].message_id, host_record->text + (i * MESSAGE_ID_LENGTH),
       MESSAGE_ID_LENGTH);
     msgq[i].message_id[MESSAGE_ID_LENGTH] = 0;
     }
@@ -1831,8 +1838,8 @@ while (1)
   if (host_length <= 0)
     {
     dbfn_close(dbm_file);
-    DEBUG(D_transport) debug_printf("waiting messages already delivered\n");
-    return FALSE;
+    DEBUG(D_transport) debug_printf_indent("waiting messages already delivered\n");
+    goto retfalse;
     }
 
   /* we were not able to find an acceptable message, nor was there a
@@ -1843,7 +1850,7 @@ while (1)
     {
     Ustrcpy(new_message_id, message_id);
     dbfn_close(dbm_file);
-    return FALSE;
+    goto retfalse;
     }
   }            /* we need to process a continuation record */
 
@@ -1859,7 +1866,12 @@ if (host_length > 0)
   }
 
 dbfn_close(dbm_file);
+DEBUG(D_transport) {acl_level--; debug_printf("transport_check_waiting: TRUE\n"); }
 return TRUE;
+
+retfalse:
+DEBUG(D_transport) {acl_level--; debug_printf("transport_check_waiting: FALSE\n"); }
+return FALSE;
 }
 
 /*************************************************
@@ -1871,9 +1883,21 @@ void
 transport_do_pass_socket(const uschar *transport_name, const uschar *hostname,
   const uschar *hostaddress, uschar *id, int socket_fd)
 {
-int i = 20;
+int i = 13;
 const uschar **argv;
 
+#ifndef DISABLE_TLS
+if (smtp_peer_options & OPTION_TLS) i += 6;
+#endif
+#ifdef EXPERIMENTAL_ESMTP_LIMITS
+if (continue_limit_mail || continue_limit_rcpt || continue_limit_rcptdom)
+                                   i += 4;
+#endif
+if (queue_run_pid != (pid_t)0)     i += 3;
+#ifdef SUPPORT_SOCKS
+if (proxy_session)                 i += 5;
+#endif
+
 /* Set up the calling arguments; use the standard function for the basics,
 but we have a number of extras that may be added. */
 
@@ -1892,11 +1916,31 @@ if (smtp_peer_options & OPTION_TLS)
     argv[i++] = sending_ip_address;
     argv[i++] = string_sprintf("%d", sending_port);
     argv[i++] = tls_out.active.sock >= 0 ? tls_out.cipher : continue_proxy_cipher;
+
+    if (tls_out.sni)
+      {
+      argv[i++] =
+#ifdef SUPPORT_DANE
+        tls_out.dane_verified ? US"-MCr" :
+#endif
+        US"-MCs";
+      argv[i++] = tls_out.sni;
+      }
     }
   else
     argv[i++] = US"-MCT";
 #endif
 
+#ifdef EXPERIMENTAL_ESMTP_LIMITS
+if (continue_limit_rcpt || continue_limit_rcptdom)
+  {
+  argv[i++] = US"-MCL";
+  argv[i++] = string_sprintf("%u", continue_limit_mail);
+  argv[i++] = string_sprintf("%u", continue_limit_rcpt);
+  argv[i++] = string_sprintf("%u", continue_limit_rcptdom);
+  }
+#endif
+
 if (queue_run_pid != (pid_t)0)
   {
   argv[i++] = US"-MCQ";
@@ -1904,6 +1948,17 @@ if (queue_run_pid != (pid_t)0)
   argv[i++] = string_sprintf("%d", queue_run_pipe);
   }
 
+#ifdef SUPPORT_SOCKS
+if (proxy_session)
+  {
+  argv[i++] = US"-MCp";
+  argv[i++] = proxy_local_address;
+  argv[i++] = string_sprintf("%d", proxy_local_port);
+  argv[i++] = proxy_external_address;
+  argv[i++] = string_sprintf("%d", proxy_external_port);
+  }
+#endif
+
 argv[i++] = US"-MC";
 argv[i++] = US transport_name;
 argv[i++] = US hostname;
@@ -1922,6 +1977,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));
@@ -1946,13 +2002,23 @@ Returns:          FALSE if fork fails; TRUE otherwise
 
 BOOL
 transport_pass_socket(const uschar *transport_name, const uschar *hostname,
-  const uschar *hostaddress, uschar *id, int socket_fd)
+  const uschar *hostaddress, uschar *id, int socket_fd
+#ifdef EXPERIMENTAL_ESMTP_LIMITS
+  , unsigned peer_limit_mail, unsigned peer_limit_rcpt, unsigned peer_limit_rcptdom
+#endif
+  )
 {
 pid_t pid;
 int status;
 
 DEBUG(D_transport) debug_printf("transport_pass_socket entered\n");
 
+#ifdef EXPERIMENTAL_ESMTP_LIMITS
+continue_limit_mail = peer_limit_mail;
+continue_limit_rcpt = peer_limit_rcpt;
+continue_limit_rcptdom = peer_limit_rcptdom;
+#endif
+
 if ((pid = exim_fork(US"continued-transport-interproc")) == 0)
   {
   /* Disconnect entirely from the parent process. If we are running in the
@@ -1988,6 +2054,31 @@ 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     *
 *************************************************/
@@ -2005,6 +2096,7 @@ Arguments:
   expand_failed      error value to set if expansion fails; not relevant if
                      addr == NULL
   addr               chain of addresses, or NULL
+  allow_tainted_args as it says; used for ${run}
   etext              text for use in error messages
   errptr             where to put error message if addr is NULL;
                      otherwise it is put in the first address
@@ -2014,15 +2106,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,
+  BOOL expand_arguments, int expand_failed, address_item * addr,
+  BOOL allow_tainted_args, 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
@@ -2031,7 +2120,7 @@ delivery batch option is set. */
 
 for (address_item * ad = addr; ad; ad = ad->next) address_count++;
 max_args = address_count + 60;
-*argvptr = argv = store_get((max_args+1)*sizeof(uschar *), FALSE);
+*argvptr = argv = store_get((max_args+1)*sizeof(uschar *), GET_UNTAINTED);
 
 /* Split the command up into arguments terminated by white space. Lose
 trailing space at the start and end. Double-quoted arguments can contain \\ and
@@ -2039,33 +2128,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++, is_tainted(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;
@@ -2099,16 +2185,16 @@ DEBUG(D_transport)
 
 if (expand_arguments)
   {
-  BOOL allow_dollar_recipients = addr != NULL &&
-    addr->parent != NULL &&
-    Ustrcmp(addr->parent->address, "system-filter") == 0;
+  BOOL allow_dollar_recipients = addr && addr->parent
+    && Ustrcmp(addr->parent->address, "system-filter") == 0;
 
-  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))
       {
@@ -2129,6 +2215,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++;
        }
@@ -2140,14 +2236,13 @@ 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))
       {
       int address_pipe_argcount = 0;
       int address_pipe_max_args;
       uschar **address_pipe_argv;
-      BOOL tainted;
 
       /* We can never have more then the argv we will be loading into */
       address_pipe_max_args = max_args - argcount + 1;
@@ -2156,13 +2251,12 @@ if (expand_arguments)
         debug_printf("address_pipe_max_args=%d\n", address_pipe_max_args);
 
       /* We allocate an additional for (uschar *)0 */
-      address_pipe_argv = store_get((address_pipe_max_args+1)*sizeof(uschar *), FALSE);
+      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);
-      tainted = is_tainted(s);
 
-      if (s == NULL || *s == '\0')
+      if (!s || !*s)
         {
         addr->transport_return = FAIL;
         addr->message = string_sprintf("Expansion of \"%s\" "
@@ -2171,32 +2265,29 @@ if (expand_arguments)
         return FALSE;
         }
 
-      while (isspace(*s)) s++; /* strip leading space */
+      Uskip_whitespace(&s);                    /* strip leading space */
 
-      while (*s != 0 && address_pipe_argcount < address_pipe_max_args)
+      while (*s && address_pipe_argcount < address_pipe_max_args)
         {
         if (*s == '\'')
-          {
-          ss = s + 1;
-          while (*ss != 0 && *ss != '\'') ss++;
-          address_pipe_argv[address_pipe_argcount++] = ss = store_get(ss - s++, tainted);
-          while (*s != 0 && *s != '\'') *ss++ = *s++;
-          if (*s != 0) 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;
@@ -2206,8 +2297,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;
@@ -2217,12 +2309,12 @@ if (expand_arguments)
         }
 
       /* If we are not just able to replace the slot that contained
-       * $address_pipe (address_pipe_argcount == 1)
-       * We have to move the existing argv by address_pipe_argcount - 1
-       * Visually if address_pipe_argcount == 2:
-       * [argv 0][argv 1][argv 2($address_pipe)][argv 3][0]
-       * [argv 0][argv 1][ap_arg0][ap_arg1][old argv 3][0]
-       */
+      $address_pipe (address_pipe_argcount == 1)
+      We have to move the existing argv by address_pipe_argcount - 1
+      Visually if address_pipe_argcount == 2:
+      [argv 0][argv 1][argv 2($address_pipe)][argv 3][0]
+      [argv 0][argv 1][ap_arg0][ap_arg1][old argv 3][0] */
+
       if (address_pipe_argcount > 1)
         memmove(
           /* current position + additional args */
@@ -2234,15 +2326,16 @@ if (expand_arguments)
         );
 
       /* Now we fill in the slots we just moved argv out of
-       * [argv 0][argv 1][argv 2=pipeargv[0]][argv 3=pipeargv[1]][old argv 3][0]
-       */
+      [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_i++)
-        {
-        argv[i++] = address_pipe_argv[address_pipe_i];
-        argcount++;
-        }
+           address_pipe_argv[address_pipe_i];
+           address_pipe_i++, argcount++)
+       {
+        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--;
@@ -2254,9 +2347,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)
         {
@@ -2271,6 +2365,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 (  !allow_tainted_args
+             && arg_is_tainted(expanded_arg, i, addr, etext, errptr))
+       return FALSE;
       argv[i] = expanded_arg;
       }
     }
@@ -2278,14 +2383,30 @@ 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]);
+      }
     }
   }
 
 return TRUE;
 }
 
+
+
+/* For error messages, a string describing the config location associated
+with current processing.  NULL if we are not in a transport. */
+/* Name only, for now */
+
+uschar *
+transport_current_name(void)
+{
+if (!transport_name) return NULL;
+return string_sprintf(" (transport %s, %s %d)", transport_name, driver_srcfile, driver_srcline);
+}
+
 #endif /*!MACRO_PREDEF*/
 /* vi: aw ai sw=2
 */