ACL: taint-enforce queue modifier
[exim.git] / src / src / acl.c
index 3788612f8501ab8d23e85ccac03575268ef77676..3166069baa46f7701addccad36a44e9f12cb5562 100644 (file)
@@ -70,7 +70,7 @@ enum { ACLC_ACL,
        ACLC_DKIM_SIGNER,
        ACLC_DKIM_STATUS,
 #endif
-#ifdef EXPERIMENTAL_DMARC
+#ifdef SUPPORT_DMARC
        ACLC_DMARC_STATUS,
 #endif
        ACLC_DNSLISTS,
@@ -192,7 +192,7 @@ static condition_def conditions[] = {
   [ACLC_DKIM_SIGNER] =         { US"dkim_signers",     TRUE, FALSE, (unsigned int) ~ACL_BIT_DKIM },
   [ACLC_DKIM_STATUS] =         { US"dkim_status",      TRUE, FALSE, (unsigned int) ~ACL_BIT_DKIM },
 #endif
-#ifdef EXPERIMENTAL_DMARC
+#ifdef SUPPORT_DMARC
   [ACLC_DMARC_STATUS] =                { US"dmarc_status",     TRUE, FALSE, (unsigned int) ~ACL_BIT_DATA },
 #endif
 
@@ -346,7 +346,7 @@ enum {
 #ifndef DISABLE_DKIM
   CONTROL_DKIM_VERIFY,
 #endif
-#ifdef EXPERIMENTAL_DMARC
+#ifdef SUPPORT_DMARC
   CONTROL_DMARC_VERIFY,
   CONTROL_DMARC_FORENSIC,
 #endif
@@ -417,7 +417,7 @@ static control_def controls_list[] = {
   },
 #endif
 
-#ifdef EXPERIMENTAL_DMARC
+#ifdef SUPPORT_DMARC
 [CONTROL_DMARC_VERIFY] =
   { US"dmarc_disable_verify",    FALSE,
          ACL_BIT_DATA | ACL_BIT_NOTSMTP | ACL_BIT_NOTSMTP_START
@@ -777,7 +777,7 @@ while ((s = (*func)()) != NULL)
       *error = string_sprintf("malformed ACL line \"%s\"", saveline);
       return NULL;
       }
-    this = store_get(sizeof(acl_block));
+    this = store_get(sizeof(acl_block), FALSE);
     *lastp = this;
     lastp = &(this->next);
     this->next = NULL;
@@ -824,7 +824,7 @@ while ((s = (*func)()) != NULL)
     return NULL;
     }
 
-  cond = store_get(sizeof(acl_condition_block));
+  cond = store_get(sizeof(acl_condition_block), FALSE);
   cond->next = NULL;
   cond->type = c;
   cond->u.negated = negated;
@@ -866,11 +866,10 @@ while ((s = (*func)()) != NULL)
     {
     uschar *endptr;
 
-    if (Ustrncmp(s, "acl_c", 5) != 0 &&
-        Ustrncmp(s, "acl_m", 5) != 0)
+    if (Ustrncmp(s, "acl_c", 5) != 0 && Ustrncmp(s, "acl_m", 5) != 0)
       {
       *error = string_sprintf("invalid variable name after \"set\" in ACL "
-        "modifier \"set %s\" (must start \"acl_c\" or \"acl_m\")", s);
+       "modifier \"set %s\" (must start \"acl_c\" or \"acl_m\")", s);
       return NULL;
       }
 
@@ -878,19 +877,19 @@ while ((s = (*func)()) != NULL)
     if (!isdigit(*endptr) && *endptr != '_')
       {
       *error = string_sprintf("invalid variable name after \"set\" in ACL "
-        "modifier \"set %s\" (digit or underscore must follow acl_c or acl_m)",
-        s);
+       "modifier \"set %s\" (digit or underscore must follow acl_c or acl_m)",
+       s);
       return NULL;
       }
 
-    while (*endptr != 0 && *endptr != '=' && !isspace(*endptr))
+    while (*endptr && *endptr != '=' && !isspace(*endptr))
       {
       if (!isalnum(*endptr) && *endptr != '_')
-        {
-        *error = string_sprintf("invalid character \"%c\" in variable name "
-          "in ACL modifier \"set %s\"", *endptr, s);
-        return NULL;
-        }
+       {
+       *error = string_sprintf("invalid character \"%c\" in variable name "
+         "in ACL modifier \"set %s\"", *endptr, s);
+       return NULL;
+       }
       endptr++;
       }
 
@@ -1022,7 +1021,9 @@ for (p = q; *p; p = q)
 
   if (!*hptr)
     {
-    header_line *h = store_get(sizeof(header_line));
+    /* The header_line struct itself is not tainted, though it points to
+    tainted data. */
+    header_line *h = store_get(sizeof(header_line), FALSE);
     h->text = hdr;
     h->next = NULL;
     h->type = newtype;
@@ -1308,7 +1309,7 @@ acl_verify_csa(const uschar *domain)
 tree_node *t;
 const uschar *found;
 int priority, weight, port;
-dns_answer dnsa;
+dns_answer * dnsa = store_get_dns_answer();
 dns_scan dnss;
 dns_record *rr;
 int rc, type;
@@ -1343,8 +1344,7 @@ extension to CSA, so we allow it to be turned off for proper conformance. */
 if (string_is_ip_address(domain, NULL) != 0)
   {
   if (!dns_csa_use_reverse) return CSA_UNKNOWN;
-  dns_build_reverse(domain, target);
-  domain = target;
+  domain = dns_build_reverse(domain);
   }
 
 /* Find out if we've already done the CSA check for this domain. If we have,
@@ -1355,14 +1355,14 @@ we return from this function. */
 t = tree_search(csa_cache, domain);
 if (t != NULL) return t->data.val;
 
-t = store_get_perm(sizeof(tree_node) + Ustrlen(domain));
+t = store_get_perm(sizeof(tree_node) + Ustrlen(domain), is_tainted(domain));
 Ustrcpy(t->name, domain);
 (void)tree_insertnode(&csa_cache, t);
 
 /* Now we are ready to do the actual DNS lookup(s). */
 
 found = domain;
-switch (dns_special_lookup(&dnsa, domain, T_CSA, &found))
+switch (dns_special_lookup(dnsa, domain, T_CSA, &found))
   {
   /* If something bad happened (most commonly DNS_AGAIN), defer. */
 
@@ -1383,9 +1383,9 @@ switch (dns_special_lookup(&dnsa, domain, T_CSA, &found))
 
 /* Scan the reply for well-formed CSA SRV records. */
 
-for (rr = dns_next_rr(&dnsa, &dnss, RESET_ANSWERS);
+for (rr = dns_next_rr(dnsa, &dnss, RESET_ANSWERS);
      rr;
-     rr = dns_next_rr(&dnsa, &dnss, RESET_NEXT)) if (rr->type == T_SRV)
+     rr = dns_next_rr(dnsa, &dnss, RESET_NEXT)) if (rr->type == T_SRV)
   {
   const uschar * p = rr->data;
 
@@ -1425,7 +1425,7 @@ for (rr = dns_next_rr(&dnsa, &dnss, RESET_ANSWERS);
   client's IP address is listed as one of the SRV target addresses. Save the
   target hostname then break to scan the additional data for its addresses. */
 
-  (void)dn_expand(dnsa.answer, dnsa.answer + dnsa.answerlen, p,
+  (void)dn_expand(dnsa->answer, dnsa->answer + dnsa->answerlen, p,
     (DN_EXPAND_ARG4_TYPE)target, sizeof(target));
 
   DEBUG(D_acl) debug_printf_indent("CSA target is %s\n", target);
@@ -1450,7 +1450,7 @@ to the target. If the name server didn't return any additional data (e.g.
 because it does not fully support SRV records), we need to do another lookup
 to obtain the target addresses; otherwise we have a definitive result. */
 
-rc = acl_verify_csa_address(&dnsa, &dnss, RESET_ADDITIONAL, target);
+rc = acl_verify_csa_address(dnsa, &dnss, RESET_ADDITIONAL, target);
 if (rc != CSA_FAIL_NOADDR) return t->data.val = rc;
 
 /* The DNS lookup type corresponds to the IP version used by the client. */
@@ -1464,7 +1464,7 @@ else
 
 
 lookup_dnssec_authenticated = NULL;
-switch (dns_lookup(&dnsa, target, type, NULL))
+switch (dns_lookup(dnsa, target, type, NULL))
   {
   /* If something bad happened (most commonly DNS_AGAIN), defer. */
 
@@ -1474,7 +1474,7 @@ switch (dns_lookup(&dnsa, target, type, NULL))
   /* If the query succeeded, scan the addresses and return the result. */
 
   case DNS_SUCCEED:
-    rc = acl_verify_csa_address(&dnsa, &dnss, RESET_ANSWERS, target);
+    rc = acl_verify_csa_address(dnsa, &dnss, RESET_ANSWERS, target);
     if (rc != CSA_FAIL_NOADDR) return t->data.val = rc;
     /* else fall through */
 
@@ -1506,11 +1506,11 @@ typedef struct {
   unsigned alt_opt_sep;                /* >0 Non-/ option separator (custom parser) */
   } verify_type_t;
 static verify_type_t verify_type_list[] = {
-    /* name                    value                   where   no-opt opt-sep */
-    { US"reverse_host_lookup", VERIFY_REV_HOST_LKUP,   ~0,     FALSE, 0 },
-    { US"certificate",         VERIFY_CERT,            ~0,     TRUE,  0 },
-    { US"helo",                        VERIFY_HELO,            ~0,     TRUE,  0 },
-    { US"csa",                 VERIFY_CSA,             ~0,     FALSE, 0 },
+    /* name                    value                   where           no-opt opt-sep */
+    { US"reverse_host_lookup", VERIFY_REV_HOST_LKUP,   (unsigned)~0,   FALSE, 0 },
+    { US"certificate",         VERIFY_CERT,            (unsigned)~0,   TRUE,  0 },
+    { US"helo",                        VERIFY_HELO,            (unsigned)~0,   TRUE,  0 },
+    { US"csa",                 VERIFY_CSA,             (unsigned)~0,   FALSE, 0 },
     { US"header_syntax",       VERIFY_HDR_SYNTAX,      ACL_BIT_DATA | ACL_BIT_NOTSMTP, TRUE, 0 },
     { US"not_blind",           VERIFY_NOT_BLIND,       ACL_BIT_DATA | ACL_BIT_NOTSMTP, FALSE, 0 },
     { US"header_sender",       VERIFY_HDR_SNDR,        ACL_BIT_DATA | ACL_BIT_NOTSMTP, FALSE, 0 },
@@ -1726,7 +1726,7 @@ switch(vp->value)
 
     if ((rc = verify_check_notblind(case_sensitive)) != OK)
       {
-      *log_msgptr = string_sprintf("bcc recipient detected");
+      *log_msgptr = US"bcc recipient detected";
       if (smtp_return_error_details)
         *user_msgptr = string_sprintf("Rejected after DATA: %s", *log_msgptr);
       }
@@ -2168,7 +2168,7 @@ gstring * g =
   string_cat(NULL, US"error in arguments to \"ratelimit\" condition: ");
 
 va_start(ap, format);
-g = string_vformat(g, TRUE, format, ap);
+g = string_vformat(g, SVFMT_EXTEND|SVFMT_REBUFFER, format, ap);
 va_end(ap);
 
 gstring_release_unused(g);
@@ -2455,7 +2455,7 @@ if (!dbdb)
     /* No Bloom filter. This basic ratelimit block is initialized below. */
     HDEBUG(D_acl) debug_printf_indent("ratelimit creating new rate data block\n");
     dbdb_size = sizeof(*dbd);
-    dbdb = store_get(dbdb_size);
+    dbdb = store_get(dbdb_size, FALSE);                /* not tainted */
     }
   else
     {
@@ -2469,7 +2469,7 @@ if (!dbdb)
     extra = (int)limit * 2 - sizeof(dbdb->bloom);
     if (extra < 0) extra = 0;
     dbdb_size = sizeof(*dbdb) + extra;
-    dbdb = store_get(dbdb_size);
+    dbdb = store_get(dbdb_size, FALSE);                /* not tainted */
     dbdb->bloom_epoch = tv.tv_sec;
     dbdb->bloom_size = sizeof(dbdb->bloom) + extra;
     memset(dbdb->bloom, 0, dbdb->bloom_size);
@@ -2686,9 +2686,10 @@ else
 
 dbfn_close(dbm);
 
-/* Store the result in the tree for future reference. */
+/* Store the result in the tree for future reference.  Take the taint status
+from the key for consistency even though it's unlikely we'll ever expand this. */
 
-t = store_get(sizeof(tree_node) + Ustrlen(key));
+t = store_get(sizeof(tree_node) + Ustrlen(key), is_tainted(key));
 t->data.ptr = dbd;
 Ustrcpy(t->name, key);
 (void)tree_insertnode(anchor, t);
@@ -2761,7 +2762,7 @@ if (*portend != '\0')
   }
 
 /* Make a single-item host list. */
-h = store_get(sizeof(host_item));
+h = store_get(sizeof(host_item), FALSE);
 memset(h, 0, sizeof(host_item));
 h->name = hostname;
 h->port = portnum;
@@ -3026,18 +3027,18 @@ for (; cb; cb = cb->next)
        break;
        #endif
 
-       #ifndef DISABLE_DKIM
+#ifndef DISABLE_DKIM
        case CONTROL_DKIM_VERIFY:
        f.dkim_disable_verify = TRUE;
-       #ifdef EXPERIMENTAL_DMARC
+# ifdef SUPPORT_DMARC
        /* Since DKIM was blocked, skip DMARC too */
        f.dmarc_disable_verify = TRUE;
        f.dmarc_enable_forensic = FALSE;
-       #endif
+endif
        break;
-       #endif
+#endif
 
-       #ifdef EXPERIMENTAL_DMARC
+#ifdef SUPPORT_DMARC
        case CONTROL_DMARC_VERIFY:
        f.dmarc_disable_verify = TRUE;
        break;
@@ -3045,7 +3046,7 @@ for (; cb; cb = cb->next)
        case CONTROL_DMARC_FORENSIC:
        f.dmarc_enable_forensic = TRUE;
        break;
-       #endif
+#endif
 
        case CONTROL_DSCP:
        if (*p == '/')
@@ -3439,7 +3440,7 @@ for (; cb; cb = cb->next)
     break;
     #endif
 
-    #ifdef EXPERIMENTAL_DMARC
+#ifdef SUPPORT_DMARC
     case ACLC_DMARC_STATUS:
     if (!f.dmarc_has_been_checked)
       dmarc_process();
@@ -3449,7 +3450,7 @@ for (; cb; cb = cb->next)
     rc = match_isinlist(dmarc_exim_expand_query(DMARC_VERIFY_STATUS),
                         &arg,0,NULL,NULL,MCL_STRING,TRUE,NULL);
     break;
-    #endif
+#endif
 
     case ACLC_DNSLISTS:
     rc = verify_check_dnsbl(where, &arg, log_msgptr);
@@ -3493,7 +3494,7 @@ for (; cb; cb = cb->next)
       (sender_host_address == NULL)? US"" : sender_host_address,
       CUSS &host_data);
     if (rc == DEFER) *log_msgptr = search_error_message;
-    if (host_data) host_data = string_copy_malloc(host_data);
+    if (host_data) host_data = string_copy_perm(host_data, TRUE);
     break;
 
     case ACLC_LOCAL_PARTS:
@@ -3589,13 +3590,19 @@ for (; cb; cb = cb->next)
     #endif
 
     case ACLC_QUEUE:
+    if (is_tainted(arg))
+      {
+      *log_msgptr = string_sprintf("Tainted name '%s' for queue not permitted",
+                                   arg);
+      return ERROR;
+      }
     if (Ustrchr(arg, '/'))
       {
       *log_msgptr = string_sprintf(
              "Directory separator not permitted in queue name: '%s'", arg);
       return ERROR;
       }
-    queue_name = string_copy_malloc(arg);
+    queue_name = string_copy_perm(arg, FALSE);
     break;
 
     case ACLC_RATELIMIT:
@@ -3632,15 +3639,12 @@ for (; cb; cb = cb->next)
       sender_address_cache, -1, 0, CUSS &sender_data);
     break;
 
-    /* Connection variables must persist forever */
+    /* Connection variables must persist forever; message variables not */
 
     case ACLC_SET:
       {
       int old_pool = store_pool;
-      if (  cb->u.varname[0] == 'c'
-#ifndef DISABLE_DKIM
-         || cb->u.varname[0] == 'd'
-#endif
+      if (  cb->u.varname[0] != 'm'
 #ifndef DISABLE_EVENT
         || event_name          /* An event is being delivered */
 #endif
@@ -3995,13 +3999,20 @@ if (Ustrchr(ss, ' ') == NULL)
   else if (*ss == '/')
     {
     struct stat statbuf;
+    if (is_tainted(ss))
+      {
+      log_write(0, LOG_MAIN|LOG_PANIC,
+       "attempt to open tainted ACL file name \"%s\"", ss);
+      /* Avoid leaking info to an attacker */
+      *log_msgptr = US"internal configuration error";
+      return ERROR;
+      }
     if ((fd = Uopen(ss, O_RDONLY, 0)) < 0)
       {
       *log_msgptr = string_sprintf("failed to open ACL file \"%s\": %s", ss,
         strerror(errno));
       return ERROR;
       }
-
     if (fstat(fd, &statbuf) != 0)
       {
       *log_msgptr = string_sprintf("failed to fstat ACL file \"%s\": %s", ss,
@@ -4009,7 +4020,8 @@ if (Ustrchr(ss, ' ') == NULL)
       return ERROR;
       }
 
-    acl_text = store_get(statbuf.st_size + 1);
+    /* If the string being used as a filename is tainted, so is the file content */
+    acl_text = store_get(statbuf.st_size + 1, is_tainted(ss));
     acl_text_end = acl_text + statbuf.st_size + 1;
 
     if (read(fd, acl_text, statbuf.st_size) != statbuf.st_size)
@@ -4039,7 +4051,7 @@ if (!acl)
   if (!acl && *log_msgptr) return ERROR;
   if (fd >= 0)
     {
-    tree_node *t = store_get_perm(sizeof(tree_node) + Ustrlen(ss));
+    tree_node *t = store_get_perm(sizeof(tree_node) + Ustrlen(ss), is_tainted(ss));
     Ustrcpy(t->name, ss);
     t->data.ptr = acl;
     (void)tree_insertnode(&acl_anchor, t);
@@ -4520,7 +4532,7 @@ acl_var_create(uschar * name)
 tree_node * node, ** root = name[0] == 'c' ? &acl_var_c : &acl_var_m;
 if (!(node = tree_search(*root, name)))
   {
-  node = store_get(sizeof(tree_node) + Ustrlen(name));
+  node = store_get(sizeof(tree_node) + Ustrlen(name), is_tainted(name));
   Ustrcpy(node->name, name);
   (void)tree_insertnode(root, node);
   }
@@ -4554,6 +4566,7 @@ void
 acl_var_write(uschar *name, uschar *value, void *ctx)
 {
 FILE *f = (FILE *)ctx;
+if (is_tainted(value)) putc('-', f);
 fprintf(f, "-acl%c %s %d\n%s\n", name[0], name+1, Ustrlen(value), value);
 }