Copyright year updates:
[exim.git] / src / src / malware.c
index 61b0c299467c17da27e209889991bc515b3aff23..2b4d282331bec752170a8e6c2018aebd3e7cd549 100644 (file)
@@ -1,10 +1,8 @@
-/* $Cambridge: exim/src/src/malware.c,v 1.20 2010/06/06 22:46:34 pdp Exp $ */
-
 /*************************************************
 *     Exim - an Internet mail transport agent    *
 *************************************************/
 
-/* Copyright (c) Tom Kistner <tom@duncanthrax.net> 2003-???? */
+/* Copyright (c) Tom Kistner <tom@duncanthrax.net> 2003-2013 */
 /* License: GPL */
 
 /* Code for calling virus (malware) scanners. Called from acl.c. */
 #include "exim.h"
 #ifdef WITH_CONTENT_SCAN
 
+/* The maximum number of clamd servers that are supported in the configuration */
+#define MAX_CLAMD_SERVERS 32
+#define MAX_CLAMD_SERVERS_S "32"
+/* Maximum length of the hostname that can be specified in the clamd address list */
+#define MAX_CLAMD_ADDRESS_LENGTH 64
+#define MAX_CLAMD_ADDRESS_LENGTH_S "64"
+
+typedef struct clamd_address_container {
+  uschar tcp_addr[MAX_CLAMD_ADDRESS_LENGTH];
+  unsigned int tcp_port;
+} clamd_address_container;
+
 /* declaration of private routines */
 static int mksd_scan_packed(int sock, uschar *scan_filename);
 static int malware_internal(uschar **listptr, uschar *eml_filename, BOOL faking);
@@ -71,17 +81,22 @@ Returns:      Exim message processing code (OK, FAIL, DEFER, ...)
 int malware(uschar **listptr) {
   uschar scan_filename[1024];
   BOOL fits;
+  int ret;
 
   fits = string_format(scan_filename, sizeof(scan_filename),
       CS"%s/scan/%s/%s.eml", spool_directory, message_id, message_id);
   if (!fits)
     {
+    av_failed = TRUE;
     log_write(0, LOG_MAIN|LOG_PANIC,
         "malware filename does not fit in buffer [malware()]");
     return DEFER;
   }
 
-  return malware_internal(listptr, scan_filename, FALSE);
+  ret = malware_internal(listptr, scan_filename, FALSE);
+  if (ret == DEFER) av_failed = TRUE;
+
+  return ret;
 }
 
 
@@ -106,28 +121,31 @@ malware_in_file(uschar *eml_filename) {
   uschar message_id_buf[64];
   int ret;
 
-  scan_options[0] = "*";
+  scan_options[0] = US"*";
   scan_options[1] = NULL;
 
   /* spool_mbox() assumes various parameters exist, when creating
   the relevant directory and the email within */
   (void) string_format(message_id_buf, sizeof(message_id_buf),
-      US"dummy-%d", pseudo_random_number(INT_MAX));
+      "dummy-%d", vaguely_random_number(INT_MAX));
   message_id = message_id_buf;
-  sender_address = "malware-sender@example.net";
-  return_path = "";
+  sender_address = US"malware-sender@example.net";
+  return_path = US"";
   recipients_list = NULL;
-  receive_add_recipient("malware-victim@example.net", -1);
+  receive_add_recipient(US"malware-victim@example.net", -1);
   enable_dollar_recipients = TRUE;
 
   ret = malware_internal(scan_options, eml_filename, TRUE);
 
-  strncpy(spooled_message_id, message_id, sizeof(spooled_message_id));
+  Ustrncpy(spooled_message_id, message_id, sizeof(spooled_message_id));
   spool_mbox_ok = 1;
   /* don't set no_mbox_unspool; at present, there's no way for it to become
   set, but if that changes, then it should apply to these tests too */
   unspool_mbox();
 
+  /* silence static analysis tools */
+  message_id = NULL;
+
   return ret;
 }
 
@@ -592,7 +610,7 @@ static int malware_internal(uschar **listptr, uschar *eml_filename, BOOL faking)
       }
     }
     else {
-      char *drweb_s = NULL;
+      const char *drweb_s = NULL;
 
       if (drweb_rc & DERR_READ_ERR) drweb_s = "read error";
       if (drweb_rc & DERR_NOMEMORY) drweb_s = "no memory";
@@ -891,7 +909,7 @@ static int malware_internal(uschar **listptr, uschar *eml_filename, BOOL faking)
         log_write(0, LOG_MAIN|LOG_PANIC,
             "malware filename does not fit in buffer [malware_internal() kavdaemon]");
       }
-      p = strrchr(scanrequest, '/');
+      p = Ustrrchr(scanrequest, '/');
       if (p)
         *p = '\0';
 
@@ -1068,7 +1086,7 @@ static int malware_internal(uschar **listptr, uschar *eml_filename, BOOL faking)
       cmdline_trigger_re = pcre_compile(CS cmdline_trigger, PCRE_COPT, (const char **)&rerror, &roffset, NULL);
       if (cmdline_trigger_re == NULL) {
         log_write(0, LOG_MAIN|LOG_PANIC,
-                 "malware acl condition: regular expression error in '%s': %s at offset %d", cmdline_trigger_re, rerror, roffset);
+                 "malware acl condition: regular expression error in '%s': %s at offset %d", cmdline_trigger, rerror, roffset);
         return DEFER;
       };
 
@@ -1086,7 +1104,7 @@ static int malware_internal(uschar **listptr, uschar *eml_filename, BOOL faking)
       cmdline_regex_re = pcre_compile(CS cmdline_regex, PCRE_COPT, (const char **)&rerror, &roffset, NULL);
       if (cmdline_regex_re == NULL) {
         log_write(0, LOG_MAIN|LOG_PANIC,
-                 "malware acl condition: regular expression error in '%s': %s at offset %d", cmdline_regex_re, rerror, roffset);
+                 "malware acl condition: regular expression error in '%s': %s at offset %d", cmdline_regex, rerror, roffset);
         return DEFER;
       };
 
@@ -1098,7 +1116,8 @@ static int malware_internal(uschar **listptr, uschar *eml_filename, BOOL faking)
             "malware filename does not fit in buffer [malware_internal() cmdline]");
         return DEFER;
         }
-      p = strrchr(eml_filename, '/');
+      Ustrcpy(file_name, eml_filename);
+      p = Ustrrchr(file_name, '/');
       if (p)
         *p = '\0';
       fits = string_format(commandline, sizeof(commandline), CS cmdline_scanner, file_name);
@@ -1199,7 +1218,6 @@ static int malware_internal(uschar **listptr, uschar *eml_filename, BOOL faking)
       struct sockaddr_un server;
       int sock, len;
       uschar *p;
-      BOOL fits;
       uschar file_name[1024];
       uschar av_buffer[1024];
 
@@ -1208,7 +1226,7 @@ static int malware_internal(uschar **listptr, uschar *eml_filename, BOOL faking)
                                           sizeof(sophie_options_buffer))) == NULL) {
         /* no options supplied, use default options */
         sophie_options = sophie_options_default;
-      };
+      }
 
       /* open the sophie socket */
       sock = socket(AF_UNIX, SOCK_STREAM, 0);
@@ -1236,21 +1254,21 @@ static int malware_internal(uschar **listptr, uschar *eml_filename, BOOL faking)
         return DEFER;
         }
       memcpy(file_name, eml_filename, len);
-      p = strrchr(file_name, '/');
+      p = Ustrrchr(file_name, '/');
       if (p)
         *p = '\0';
 
       DEBUG(D_acl) debug_printf("Malware scan: issuing %s scan [%s]\n",
           scanner_name, sophie_options);
 
-      if (write(sock, file_name, Ustrlen(file_name)) < 0) {
+      if (  write(sock, file_name, Ustrlen(file_name)) < 0
+        || write(sock, "\n", 1) != 1
+         ) {
         (void)close(sock);
         log_write(0, LOG_MAIN|LOG_PANIC,
              "malware acl condition: unable to write to sophie UNIX socket (%s)", sophie_options);
         return DEFER;
-      };
-
-      (void)write(sock, "\n", 1);
+      }
 
       /* wait for result */
       memset(av_buffer, 0, sizeof(av_buffer));
@@ -1259,7 +1277,7 @@ static int malware_internal(uschar **listptr, uschar *eml_filename, BOOL faking)
         log_write(0, LOG_MAIN|LOG_PANIC,
              "malware acl condition: unable to read from sophie UNIX socket (%s)", sophie_options);
         return DEFER;
-      };
+      }
 
       (void)close(sock);
 
@@ -1277,7 +1295,7 @@ static int malware_internal(uschar **listptr, uschar *eml_filename, BOOL faking)
       else {
         /* all ok, no virus */
         malware_name = NULL;
-      };
+      }
     }
     /* ----------------------------------------------------------------------- */
 
@@ -1295,28 +1313,28 @@ static int malware_internal(uschar **listptr, uschar *eml_filename, BOOL faking)
      * WITH_OLD_CLAMAV_STREAM is defined.
      * See Exim bug 926 for details.  */
     else if (strcmpic(scanner_name,US"clamd") == 0) {
-      uschar *clamd_options;
+      uschar *clamd_options = NULL;
       uschar clamd_options_buffer[1024];
       uschar clamd_options_default[] = "/tmp/clamd";
-      uschar *p,*vname;
+      uschar *p, *vname, *result_tag, *response_end;
       struct sockaddr_un server;
       int sock,bread=0;
       unsigned int port;
       uschar file_name[1024];
       uschar av_buffer[1024];
-      uschar hostname[256];
+      uschar *hostname = "";
       struct hostent *he;
       struct in_addr in;
-      uschar *clamd_options2;
-      uschar clamd_options2_buffer[1024];
-      uschar clamd_options2_default[] = "";
       uschar *clamav_fbuf;
-      uschar scanrequest[1024];
-      int sockData, clam_fd, result;
+      int clam_fd, result;
       unsigned int fsize;
-      BOOL use_scan_command, fits;
+      BOOL use_scan_command = FALSE, fits;
+      clamd_address_container * clamd_address_vector[MAX_CLAMD_SERVERS];
+      int current_server;
+      int num_servers = 0;
 #ifdef WITH_OLD_CLAMAV_STREAM
       uschar av_buffer2[1024];
+      int sockData;
 #else
       uint32_t send_size, send_final_zeroblock;
 #endif
@@ -1327,56 +1345,136 @@ static int malware_internal(uschar **listptr, uschar *eml_filename, BOOL faking)
         /* no options supplied, use default options */
         clamd_options = clamd_options_default;
       }
-      if ((clamd_options2 = string_nextinlist(&av_scanner_work, &sep,
-                                             clamd_options2_buffer,
-                                             sizeof(clamd_options2_buffer))) == NULL) {
-        clamd_options2 = clamd_options2_default;
-      }
 
-      if ((*clamd_options == '/') || (strcmpic(clamd_options2,US"local") == 0))
+      if (*clamd_options == '/')
+        /* Local file; so we def want to use_scan_command and don't want to try
+         * passing IP/port combinations */
         use_scan_command = TRUE;
-      else
-        use_scan_command = FALSE;
+      else {
+        uschar *address = clamd_options;
+        uschar address_buffer[MAX_CLAMD_ADDRESS_LENGTH + 20];
 
-      /* socket does not start with '/' -> network socket */
-      if (*clamd_options != '/') {
+        /* Go through the rest of the list of host/port and construct an array
+         * of servers to try. The first one is the bit we just passed from
+         * clamd_options so process that first and then scan the remainder of
+         * the address buffer */
+        do {
+          clamd_address_container *this_clamd;
 
-        /* Confirmed in ClamAV source (0.95.3) that the TCPAddr option of clamd
-         * only supports AF_INET, but we should probably be looking to the
-         * future and rewriting this to be protocol-independent anyway. */
+          /* The 'local' option means use the SCAN command over the network
+           * socket (ie common file storage in use) */
+          if (strcmpic(address,US"local") == 0) {
+            use_scan_command = TRUE;
+            continue;
+          }
 
-        /* extract host and port part */
-        if( sscanf(CS clamd_options, "%s %u", hostname, &port) != 2 ) {
-          log_write(0, LOG_MAIN|LOG_PANIC,
-                    "malware acl condition: clamd: invalid socket '%s'", clamd_options);
-          return DEFER;
-        };
+          /* XXX: If unsuccessful we should free this memory */
+          this_clamd =
+              (clamd_address_container *)store_get(sizeof(clamd_address_container));
 
-        /* Lookup the host */
-        if((he = gethostbyname(CS hostname)) == 0) {
-          log_write(0, LOG_MAIN|LOG_PANIC,
-                    "malware acl condition: clamd: failed to lookup host '%s'", hostname);
-          return DEFER;
-        }
+          /* extract host and port part */
+          if( sscanf(CS address, "%" MAX_CLAMD_ADDRESS_LENGTH_S "s %u", this_clamd->tcp_addr,
+                                            &(this_clamd->tcp_port)) != 2 ) {
+            log_write(0, LOG_MAIN|LOG_PANIC,
+                      "malware acl condition: clamd: invalid address '%s'", address);
+            continue;
+          }
 
-        in = *(struct in_addr *) he->h_addr_list[0];
+          clamd_address_vector[num_servers] = this_clamd;
+          num_servers++;
+          if (num_servers >= MAX_CLAMD_SERVERS) {
+            log_write(0, LOG_MAIN|LOG_PANIC,
+                  "More than " MAX_CLAMD_SERVERS_S " clamd servers specified; "
+                  "only using the first " MAX_CLAMD_SERVERS_S );
+            break;
+          }
+        } while ((address = string_nextinlist(&av_scanner_work, &sep,
+                                        address_buffer,
+                                        sizeof(address_buffer))) != NULL);
 
-        /* Open the ClamAV Socket */
-        if ( (sock = ip_socket(SOCK_STREAM, AF_INET)) < 0) {
+        /* check if we have at least one server */
+        if (!num_servers) {
           log_write(0, LOG_MAIN|LOG_PANIC,
-                    "malware acl condition: clamd: unable to acquire socket (%s)",
-                    strerror(errno));
+             "malware acl condition: clamd: no useable clamd server addresses in malware configuration option.");
           return DEFER;
         }
+      }
 
-        if (ip_connect(sock, AF_INET, (uschar*)inet_ntoa(in), port, 5) < 0) {
-          (void)close(sock);
-          log_write(0, LOG_MAIN|LOG_PANIC,
-                    "malware acl condition: clamd: connection to %s, port %u failed (%s)",
-                    inet_ntoa(in), port, strerror(errno));
-          return DEFER;
+      /* See the discussion of response formats below to see why we really don't
+      like colons in filenames when passing filenames to ClamAV. */
+      if (use_scan_command && Ustrchr(eml_filename, ':')) {
+       log_write(0, LOG_MAIN|LOG_PANIC,
+           "malware acl condition: clamd: local/SCAN mode incompatible with" \
+           " : in path to email filename [%s]", eml_filename);
+       return DEFER;
+      }
+
+      /* We have some network servers specified */
+      if (num_servers) {
+
+        /* Confirmed in ClamAV source (0.95.3) that the TCPAddr option of clamd
+         * only supports AF_INET, but we should probably be looking to the
+         * future and rewriting this to be protocol-independent anyway. */
+
+        while ( num_servers > 0 ) {
+          /* Randomly pick a server to start with */
+          current_server = random_number( num_servers );
+
+          debug_printf("trying server name %s, port %u\n",
+                       clamd_address_vector[current_server]->tcp_addr,
+                       clamd_address_vector[current_server]->tcp_port);
+
+          /* Lookup the host. This is to ensure that we connect to the same IP
+           * on both connections (as one host could resolve to multiple ips) */
+          if((he = gethostbyname(CS clamd_address_vector[current_server]->tcp_addr))
+                          == 0) {
+            log_write(0, LOG_MAIN|LOG_PANIC,
+                    "malware acl condition: clamd: failed to lookup host '%s'",
+                    clamd_address_vector[current_server]->tcp_addr
+                    );
+            goto try_next_server;
+          }
+
+          in = *(struct in_addr *) he->h_addr_list[0];
+
+          /* Open the ClamAV Socket */
+          if ( (sock = ip_socket(SOCK_STREAM, AF_INET)) < 0) {
+            log_write(0, LOG_MAIN|LOG_PANIC,
+                      "malware acl condition: clamd: unable to acquire socket (%s)",
+                      strerror(errno));
+            goto try_next_server;
+          }
+
+          if (ip_connect( sock,
+                          AF_INET,
+                          (uschar*)inet_ntoa(in),
+                          clamd_address_vector[current_server]->tcp_port,
+                          5 ) > -1) {
+            /* Connection successfully established with a server */
+            hostname = clamd_address_vector[current_server]->tcp_addr;
+            break;
+          } else {
+            log_write(0, LOG_MAIN|LOG_PANIC,
+               "malware acl condition: clamd: connection to %s, port %u failed (%s)",
+               clamd_address_vector[current_server]->tcp_addr,
+               clamd_address_vector[current_server]->tcp_port,
+               strerror(errno));
+
+            (void)close(sock);
+          }
+
+try_next_server:
+          /* Remove the server from the list. XXX We should free the memory */
+          num_servers--;
+          int i;
+          for( i = current_server; i < num_servers; i++ )
+            clamd_address_vector[i] = clamd_address_vector[i+1];
         }
 
+        if ( num_servers == 0 ) {
+          log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: all clamd servers failed");
+            return DEFER;
+        }
       } else {
         /* open the local socket */
         if ((sock = socket(AF_UNIX, SOCK_STREAM, 0)) < 0) {
@@ -1615,10 +1713,25 @@ static int malware_internal(uschar **listptr, uschar *eml_filename, BOOL faking)
         return DEFER;
       }
 
-      /* Check the result. ClamAV Returns
-         infected: -> "<filename>: <virusname> FOUND"
-         not-infected: -> "<filename>: OK"
-         error: -> "<filename>: <errcode> ERROR */
+      /* Check the result. ClamAV returns one of two result formats.
+      In the basic mode, the response is of the form:
+        infected: -> "<filename>: <virusname> FOUND"
+        not-infected: -> "<filename>: OK"
+        error: -> "<filename>: <errcode> ERROR
+      If the ExtendedDetectionInfo option has been turned on, then we get:
+        "<filename>: <virusname>(<virushash>:<virussize>) FOUND"
+      for the infected case.  Compare:
+/tmp/eicar.com: Eicar-Test-Signature FOUND
+/tmp/eicar.com: Eicar-Test-Signature(44d88612fea8a8f36de82e1278abb02f:68) FOUND
+
+      In the streaming case, clamd uses the filename "stream" which you should
+      be able to verify with { ktrace clamdscan --stream /tmp/eicar.com }.  (The
+      client app will replace "stream" with the original filename before returning
+      results to stdout, but the trace shows the data).
+
+      We will assume that the pathname passed to clamd from Exim does not contain
+      a colon.  We will have whined loudly above if the eml_filename does (and we're
+      passing a filename to clamd). */
 
       if (!(*av_buffer)) {
         log_write(0, LOG_MAIN|LOG_PANIC,
@@ -1626,50 +1739,76 @@ static int malware_internal(uschar **listptr, uschar *eml_filename, BOOL faking)
         return DEFER;
       }
 
-      /* strip newline at the end (won't be present for zINSTREAM) */
+      /* strip newline at the end (won't be present for zINSTREAM)
+      (also any trailing whitespace, which shouldn't exist, but we depend upon
+      this below, so double-check) */
       p = av_buffer + Ustrlen(av_buffer) - 1;
-      if( *p == '\n' ) *p = '\0';
+      if (*p == '\n') *p = '\0';
 
       DEBUG(D_acl) debug_printf("Malware response: %s\n", av_buffer);
 
+      while (isspace(*--p) && (p > av_buffer))
+       *p = '\0';
+      if (*p) ++p;
+      response_end = p;
+
       /* colon in returned output? */
-      if((p = Ustrrchr(av_buffer,':')) == NULL) {
+      if((p = Ustrchr(av_buffer,':')) == NULL) {
         log_write(0, LOG_MAIN|LOG_PANIC,
-                  "malware acl condition: clamd: ClamAV returned malformed result: %s",
+                  "malware acl condition: clamd: ClamAV returned malformed result (missing colon): %s",
                   av_buffer);
         return DEFER;
       }
 
       /* strip filename */
-      ++p;
-      while (*p == ' ') ++p;
+      while (*p && isspace(*++p)) /**/;
       vname = p;
-      if ((p = Ustrstr(vname, "FOUND"))!=NULL) {
-           *p=0;
-           for (--p;p>vname && *p<=32;p--) *p=0;
-           for (;*vname==32;vname++);
-           Ustrcpy(malware_name_buffer,vname);
-           malware_name = malware_name_buffer;
-           DEBUG(D_acl) debug_printf("Malware found, name \"%s\"\n", malware_name);
-      }
-      else {
-           if (Ustrstr(vname, "ERROR")!=NULL) {
-              /* ClamAV reports ERROR
-              Find line start */
-              for (;*vname!='\n' && vname>av_buffer; vname--);
-              if (*vname=='\n') vname++;
-
-              log_write(0, LOG_MAIN|LOG_PANIC,
-                     "malware acl condition: clamd: ClamAV returned %s",vname);
-              return DEFER;
-           }
-           else {
-              /* Everything should be OK */
-              malware_name = NULL;
-              DEBUG(D_acl) debug_printf("Malware not found\n");
-           }
+
+      /* It would be bad to encounter a virus with "FOUND" in part of the name,
+      but we should at least be resistant to it. */
+      p = Ustrrchr(vname, ' ');
+      if (p)
+       result_tag = p + 1;
+      else
+       result_tag = vname;
+
+      if (Ustrcmp(result_tag, "FOUND") == 0) {
+       /* p should still be the whitespace before the result_tag */
+       while (isspace(*p)) --p;
+       *++p = '\0';
+        /* Strip off the extended information too, which will be in parens
+        after the virus name, with no intervening whitespace. */
+       if (*--p == ')') {
+         /* "(hash:size)", so previous '(' will do; if not found, we have
+         a curious virus name, but not an error. */
+         p = Ustrrchr(vname, '(');
+         if (p)
+           *p = '\0';
+       }
+       Ustrncpy(malware_name_buffer, vname, sizeof(malware_name_buffer)-1);
+       malware_name = malware_name_buffer;
+       DEBUG(D_acl) debug_printf("Malware found, name \"%s\"\n", malware_name);
+
+      } else if (Ustrcmp(result_tag, "ERROR") == 0) {
+       log_write(0, LOG_MAIN|LOG_PANIC,
+                 "malware acl condition: clamd: ClamAV returned: %s",
+                 av_buffer);
+       return DEFER;
+
+      } else if (Ustrcmp(result_tag, "OK") == 0) {
+       /* Everything should be OK */
+       malware_name = NULL;
+       DEBUG(D_acl) debug_printf("Malware not found\n");
+
+      } else {
+       log_write(0, LOG_MAIN|LOG_PANIC,
+                 "malware acl condition: clamd: unparseable response from ClamAV: {%s}",
+                 av_buffer);
+       return DEFER;
       }
-    }
+
+    } /* clamd */
+
     /* ----------------------------------------------------------------------- */
 
 
@@ -1860,14 +1999,14 @@ static int mksd_parse_line (char *line)
 static int mksd_scan_packed(int sock, uschar *scan_filename)
 {
   struct iovec iov[3];
-  char *cmd = "MSQ\n";
+  const char *cmd = "MSQ\n";
   uschar av_buffer[1024];
 
-  iov[0].iov_base = cmd;
+  iov[0].iov_base = (void *) cmd;
   iov[0].iov_len = 3;
   iov[1].iov_base = CS scan_filename;
   iov[1].iov_len = Ustrlen(scan_filename);
-  iov[2].iov_base = cmd + 3;
+  iov[2].iov_base = (void *) (cmd + 3);
   iov[2].iov_len = 1;
 
   if (mksd_writev (sock, iov, 3) < 0)