SECURITY: DMARC uses From header untrusted data
[exim.git] / src / src / dmarc.c
index 85b6ec8..c619061 100644 (file)
 
 #include "exim.h"
 #ifdef EXPERIMENTAL_DMARC
+#if !defined EXPERIMENTAL_SPF
+#error SPF must also be enabled for DMARC
+#elif defined DISABLE_DKIM
+#error DKIM must also be enabled for DMARC
+#else
 
 #include "functions.h"
 #include "dmarc.h"
@@ -25,15 +30,12 @@ BOOL dmarc_abort  = FALSE;
 uschar *dmarc_pass_fail = US"skipped";
 extern pdkim_signature  *dkim_signatures;
 header_line *from_header   = NULL;
-#ifdef EXPERIMENTAL_SPF
 extern SPF_response_t   *spf_response;
-int    dmarc_spf_result     = 0;
+int dmarc_spf_ares_result  = 0;
 uschar *spf_sender_domain  = NULL;
 uschar *spf_human_readable = NULL;
-#endif
 u_char *header_from_sender = NULL;
 int history_file_status    = DMARC_HIST_OK;
-uschar *history_buffer     = NULL;
 uschar *dkim_history_buffer= NULL;
 
 /* Accept an error_block struct, initialize if empty, parse to the
@@ -64,7 +66,8 @@ add_to_eblock(error_block *eblock, uschar *t1, uschar *t2)
    messages on the same SMTP connection (that come from the
    same host with the same HELO string) */
 
-int dmarc_init() {
+int dmarc_init()
+{
   int *netmask   = NULL;   /* Ignored */
   int is_ipv6    = 0;
   char *tld_file = (dmarc_tld_file == NULL) ?
@@ -78,11 +81,11 @@ int dmarc_init() {
   dmarc_abort        = FALSE;
   dmarc_pass_fail    = US"skipped";
   dmarc_used_domain  = US"";
+  dmarc_ar_header    = NULL;
+  dmarc_has_been_checked = FALSE;
   header_from_sender = NULL;
-#ifdef EXPERIMENTAL_SPF
   spf_sender_domain  = NULL;
   spf_human_readable = NULL;
-#endif
 
   /* ACLs have "control=dmarc_disable_verify" */
   if (dmarc_disable_verify == TRUE)
@@ -97,7 +100,9 @@ int dmarc_init() {
                          opendmarc_policy_status_to_str(libdm_status));
     dmarc_abort = TRUE;
   }
-  if (opendmarc_tld_read_file(tld_file, NULL, NULL, NULL))
+  if (dmarc_tld_file == NULL)
+    dmarc_abort = TRUE;
+  else if (opendmarc_tld_read_file(tld_file, NULL, NULL, NULL))
   {
     log_write(0, LOG_MAIN|LOG_PANIC, "DMARC failure to load tld list %s: %d",
                          tld_file, errno);
@@ -140,7 +145,8 @@ int dmarc_store_data(header_line *hdr) {
    strings and evaluates the condition outcome. */
 
 int dmarc_process() {
-    int sr, origin; /* used in SPF section */
+    int sr, origin;             /* used in SPF section */
+    int dmarc_spf_result  = 0;  /* stores spf into dmarc conn ctx */
     pdkim_signature *sig  = NULL;
     BOOL has_dmarc_record = TRUE;
     u_char **ruf; /* forensic report addressees, if called for */
@@ -162,26 +168,31 @@ int dmarc_process() {
     dmarc_abort = TRUE;
   else
   {
-    /* I strongly encourage anybody who can make this better to contact me directly!
-     * <cannonball> Is this an insane way to extract the email address from the From: header?
-     * <jgh_hm> it's sure a horrid layer-crossing....
-     * <cannonball> I'm not denying that :-/
-     * <jgh_hm> there may well be no better though
-     */
-    header_from_sender = expand_string(
-                           string_sprintf("${domain:${extract{1}{:}{${addresses:%s}}}}",
-                             from_header->text) );
-    /* The opendmarc library extracts the domain from the email address, but
-     * only try to store it if it's not empty.  Otherwise, skip out of DMARC. */
-    if (strcmp( CCS header_from_sender, "") == 0)
-      dmarc_abort = TRUE;
-    libdm_status = (dmarc_abort == TRUE) ?
-                  DMARC_PARSE_OKAY :
-                  opendmarc_policy_store_from_domain(dmarc_pctx, header_from_sender);
-    if (libdm_status != DMARC_PARSE_OKAY)
+  uschar * errormsg;
+  int dummy, domain;
+  uschar * p;
+  uschar saveend;
+
+  parse_allow_group = TRUE;
+  p = parse_find_address_end(from_header->text, FALSE);
+  saveend = *p; *p = '\0';
+  if ((header_from_sender = parse_extract_address(from_header->text, &errormsg,
+                              &dummy, &dummy, &domain, FALSE)))
+    header_from_sender += domain;
+  *p = saveend;
+
+  /* The opendmarc library extracts the domain from the email address, but
+   * only try to store it if it's not empty.  Otherwise, skip out of DMARC. */
+  if (!header_from_sender || (strcmp( CCS header_from_sender, "") == 0))
+    dmarc_abort = TRUE;
+  libdm_status = dmarc_abort ?
+    DMARC_PARSE_OKAY :
+    opendmarc_policy_store_from_domain(dmarc_pctx, header_from_sender);
+  if (libdm_status != DMARC_PARSE_OKAY)
     {
-      log_write(0, LOG_MAIN|LOG_PANIC, "failure to store header From: in DMARC: %s, header was '%s'",
-                           opendmarc_policy_status_to_str(libdm_status), from_header->text);
+      log_write(0, LOG_MAIN|LOG_PANIC,
+                "failure to store header From: in DMARC: %s, header was '%s'",
+                opendmarc_policy_status_to_str(libdm_status), from_header->text);
       dmarc_abort = TRUE;
     }
   }
@@ -190,40 +201,22 @@ int dmarc_process() {
    * instead do this in the ACLs.  */
   if (dmarc_abort == FALSE && sender_host_authenticated == NULL)
   {
-#ifdef EXPERIMENTAL_SPF
     /* Use the envelope sender domain for this part of DMARC */
     spf_sender_domain = expand_string(US"$sender_address_domain");
     if ( spf_response == NULL )
     {
       /* No spf data means null envelope sender so generate a domain name
-       * from the sender_host_name || sender_helo_name  */
+       * from the sender_helo_name  */
       if (spf_sender_domain == NULL)
       {
-        spf_sender_domain = (sender_host_name == NULL) ? sender_helo_name : sender_host_name;
-        uschar *subdomain = spf_sender_domain;
-        int count = 0;
-        while (subdomain && *subdomain != '.')
-        {
-          subdomain++;
-          count++;
-        }
-        /* If parsed characters in temp var "subdomain" and is pointing to
-         * a period now, get rid of the period and use that.  Otherwise
-         * will use whatever was first set in spf_sender_domain.  Goal is to
-         * generate a sane answer, not necessarily the right/best answer b/c
-         * at this point with a null sender, it's a bounce message, making
-         * the spf domain be subjective.  */
-        if (count > 0 && *subdomain == '.')
-        {
-          subdomain++;
-          spf_sender_domain = subdomain;
-        }
+        spf_sender_domain = sender_helo_name;
         log_write(0, LOG_MAIN, "DMARC using synthesized SPF sender domain = %s\n",
                                spf_sender_domain);
         DEBUG(D_receive)
           debug_printf("DMARC using synthesized SPF sender domain = %s\n", spf_sender_domain);
       }
       dmarc_spf_result = DMARC_POLICY_SPF_OUTCOME_NONE;
+      dmarc_spf_ares_result = ARES_RESULT_UNKNOWN;
       origin = DMARC_POLICY_SPF_ORIGIN_HELO;
       spf_human_readable = US"";
     }
@@ -235,6 +228,14 @@ int dmarc_process() {
                          (sr == SPF_RESULT_FAIL)     ? DMARC_POLICY_SPF_OUTCOME_FAIL :
                          (sr == SPF_RESULT_SOFTFAIL) ? DMARC_POLICY_SPF_OUTCOME_TMPFAIL :
                          DMARC_POLICY_SPF_OUTCOME_NONE;
+      dmarc_spf_ares_result = (sr == SPF_RESULT_NEUTRAL)   ? ARES_RESULT_NEUTRAL :
+                              (sr == SPF_RESULT_PASS)      ? ARES_RESULT_PASS :
+                              (sr == SPF_RESULT_FAIL)      ? ARES_RESULT_FAIL :
+                              (sr == SPF_RESULT_SOFTFAIL)  ? ARES_RESULT_SOFTFAIL :
+                              (sr == SPF_RESULT_NONE)      ? ARES_RESULT_NONE :
+                              (sr == SPF_RESULT_TEMPERROR) ? ARES_RESULT_TEMPERROR :
+                              (sr == SPF_RESULT_PERMERROR) ? ARES_RESULT_PERMERROR :
+                              ARES_RESULT_UNKNOWN;
       origin = DMARC_POLICY_SPF_ORIGIN_MAILFROM;
       spf_human_readable = (uschar *)spf_response->header_comment;
       DEBUG(D_receive)
@@ -250,7 +251,6 @@ int dmarc_process() {
         log_write(0, LOG_MAIN|LOG_PANIC, "failure to store spf for DMARC: %s",
                              opendmarc_policy_status_to_str(libdm_status));
     }
-#endif /* EXPERIMENTAL_SPF */
 
     /* Now we cycle through the dkim signature results and put into
      * the opendmarc context, further building the DMARC reply.  */
@@ -258,11 +258,12 @@ int dmarc_process() {
     dkim_history_buffer = US"";
     while (sig != NULL)
     {
-      int dkim_result, vs;
-      vs = sig->verify_status;
+      int dkim_result, dkim_ares_result, vs, ves;
+      vs  = sig->verify_status;
+      ves = sig->verify_ext_status;
       dkim_result = ( vs == PDKIM_VERIFY_PASS ) ? DMARC_POLICY_DKIM_OUTCOME_PASS :
-                   ( vs == PDKIM_VERIFY_FAIL ) ? DMARC_POLICY_DKIM_OUTCOME_FAIL :
-                   ( vs == PDKIM_VERIFY_INVALID ) ? DMARC_POLICY_DKIM_OUTCOME_TMPFAIL :
+                    ( vs == PDKIM_VERIFY_FAIL ) ? DMARC_POLICY_DKIM_OUTCOME_FAIL :
+                    ( vs == PDKIM_VERIFY_INVALID ) ? DMARC_POLICY_DKIM_OUTCOME_TMPFAIL :
                     DMARC_POLICY_DKIM_OUTCOME_NONE;
       libdm_status = opendmarc_policy_store_dkim(dmarc_pctx, (uschar *)sig->domain,
                                                 dkim_result, US"");
@@ -272,8 +273,17 @@ int dmarc_process() {
         log_write(0, LOG_MAIN|LOG_PANIC, "failure to store dkim (%s) for DMARC: %s",
                             sig->domain, opendmarc_policy_status_to_str(libdm_status));
 
+      dkim_ares_result = ( vs == PDKIM_VERIFY_PASS )    ? ARES_RESULT_PASS :
+                             ( vs == PDKIM_VERIFY_FAIL )    ? ARES_RESULT_FAIL :
+                             ( vs == PDKIM_VERIFY_NONE )    ? ARES_RESULT_NONE :
+                             ( vs == PDKIM_VERIFY_INVALID ) ?
+                           ( ves == PDKIM_VERIFY_INVALID_PUBKEY_UNAVAILABLE ? ARES_RESULT_PERMERROR :
+                             ves == PDKIM_VERIFY_INVALID_BUFFER_SIZE        ? ARES_RESULT_PERMERROR :
+                             ves == PDKIM_VERIFY_INVALID_PUBKEY_PARSING     ? ARES_RESULT_PERMERROR :
+                             ARES_RESULT_UNKNOWN ) :
+                          ARES_RESULT_UNKNOWN;
       dkim_history_buffer = string_sprintf("%sdkim %s %d\n", dkim_history_buffer,
-                                                             sig->domain, dkim_result);
+                                                             sig->domain, dkim_ares_result);
       sig = sig->next;
     }
     libdm_status = opendmarc_policy_query_dmarc(dmarc_pctx, US"");
@@ -320,7 +330,7 @@ int dmarc_process() {
     {
       case DMARC_POLICY_ABSENT:     /* No DMARC record found */
         dmarc_status = US"norecord";
-        dmarc_pass_fail = US"temperror";
+        dmarc_pass_fail = US"none";
         dmarc_status_text = US"No DMARC record";
         action = DMARC_RESULT_ACCEPT;
         break;
@@ -398,10 +408,11 @@ int dmarc_process() {
 
 int dmarc_write_history_file()
 {
-  static int history_file_fd;
+  int history_file_fd;
   ssize_t written_len;
   int tmp_ans;
   u_char **rua; /* aggregate report addressees */
+  uschar *history_buffer = NULL;
 
   if (dmarc_history_file == NULL)
     return DMARC_HIST_DISABLED;
@@ -423,12 +434,9 @@ int dmarc_write_history_file()
   history_buffer = string_sprintf("%smfrom %s\n", history_buffer,
                      expand_string(US"$sender_address_domain"));
 
-#ifdef EXPERIMENTAL_SPF
   if (spf_response != NULL)
-    history_buffer = string_sprintf("%sspf %d\n", history_buffer, dmarc_spf_result);
-#else
-    history_buffer = string_sprintf("%sspf -1\n", history_buffer);
-#endif /* EXPERIMENTAL_SPF */
+    history_buffer = string_sprintf("%sspf %d\n", history_buffer, dmarc_spf_ares_result);
+    // history_buffer = string_sprintf("%sspf -1\n", history_buffer);
 
   history_buffer = string_sprintf("%s%s", history_buffer, dkim_history_buffer);
   history_buffer = string_sprintf("%spdomain %s\n", history_buffer, dmarc_used_domain);
@@ -581,7 +589,6 @@ uschar *dmarc_auth_results_header(header_line *from_header, uschar *hostname)
 #if 0
   /* I don't think this belongs here, but left it here commented out
    * because it was a lot of work to get working right. */
-#ifdef EXPERIMENTAL_SPF
   if (spf_response != NULL) {
     uschar *dmarc_ar_spf = US"";
     int sr               = 0;
@@ -597,7 +604,6 @@ uschar *dmarc_auth_results_header(header_line *from_header, uschar *hostname)
                              expand_string(US"$sender_address") );
   }
 #endif
-#endif
   hdr_tmp = string_sprintf("%s dmarc=%s",
                            hdr_tmp, dmarc_pass_fail);
   if (header_from_sender)
@@ -606,6 +612,7 @@ uschar *dmarc_auth_results_header(header_line *from_header, uschar *hostname)
   return hdr_tmp;
 }
 
-#endif
+#endif /* EXPERIMENTAL_SPF */
+#endif /* EXPERIMENTAL_DMARC */
 
 // vim:sw=2 expandtab