You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@spamassassin.apache.org by gb...@apache.org on 2019/07/05 17:26:47 UTC

svn commit: r1862624 - in /spamassassin: branches/3.4/ branches/3.4/lib/Mail/SpamAssassin/Plugin/ trunk/ trunk/rules/

Author: gbechis
Date: Fri Jul  5 17:26:47 2019
New Revision: 1862624

URL: http://svn.apache.org/viewvc?rev=1862624&view=rev
Log:
Add OLEMacro plugin to 3.4.3 and rename rules/v*.pre
accordingly

Added:
    spamassassin/branches/3.4/lib/Mail/SpamAssassin/Plugin/OLEMacro.pm
    spamassassin/trunk/rules/v343.pre
Removed:
    spamassassin/trunk/rules/v400.pre
Modified:
    spamassassin/branches/3.4/MANIFEST
    spamassassin/branches/3.4/MANIFEST.SKIP
    spamassassin/branches/3.4/Makefile.PL
    spamassassin/branches/3.4/UPGRADE
    spamassassin/trunk/MANIFEST
    spamassassin/trunk/Makefile.PL

Modified: spamassassin/branches/3.4/MANIFEST
URL: http://svn.apache.org/viewvc/spamassassin/branches/3.4/MANIFEST?rev=1862624&r1=1862623&r2=1862624&view=diff
==============================================================================
--- spamassassin/branches/3.4/MANIFEST (original)
+++ spamassassin/branches/3.4/MANIFEST Fri Jul  5 17:26:47 2019
@@ -92,6 +92,7 @@ lib/Mail/SpamAssassin/Plugin/HeaderEval.
 lib/Mail/SpamAssassin/Plugin/ImageInfo.pm
 lib/Mail/SpamAssassin/Plugin/MIMEEval.pm
 lib/Mail/SpamAssassin/Plugin/MIMEHeader.pm
+lib/Mail/SpamAssassin/Plugin/OLEMacro.pm
 lib/Mail/SpamAssassin/Plugin/OneLineBodyRuleType.pm
 lib/Mail/SpamAssassin/Plugin/Phishing.pm
 lib/Mail/SpamAssassin/Plugin/PhishTag.pm

Modified: spamassassin/branches/3.4/MANIFEST.SKIP
URL: http://svn.apache.org/viewvc/spamassassin/branches/3.4/MANIFEST.SKIP?rev=1862624&r1=1862623&r2=1862624&view=diff
==============================================================================
--- spamassassin/branches/3.4/MANIFEST.SKIP (original)
+++ spamassassin/branches/3.4/MANIFEST.SKIP Fri Jul  5 17:26:47 2019
@@ -119,7 +119,6 @@
 ^build/repackage_latest_update_rules$
 ^MYMETA.(json|yml)$
 ^trunk-only.*
-^rules/v400\.pre$
 ^t/mkrules\.t$
 ^t/mkrules_else\.t$
 ^t/spamc_H\.t$

Modified: spamassassin/branches/3.4/Makefile.PL
URL: http://svn.apache.org/viewvc/spamassassin/branches/3.4/Makefile.PL?rev=1862624&r1=1862623&r2=1862624&view=diff
==============================================================================
--- spamassassin/branches/3.4/Makefile.PL (original)
+++ spamassassin/branches/3.4/Makefile.PL Fri Jul  5 17:26:47 2019
@@ -1129,7 +1129,7 @@ conf__install:
 	$(PERL) -MFile::Copy -e "copy(q[rules/v340.pre], q[$(B_CONFDIR)/v340.pre]) unless -f q[$(B_CONFDIR)/v340.pre]"
 	$(PERL) -MFile::Copy -e "copy(q[rules/v341.pre], q[$(B_CONFDIR)/v341.pre]) unless -f q[$(B_CONFDIR)/v341.pre]"
 	$(PERL) -MFile::Copy -e "copy(q[rules/v342.pre], q[$(B_CONFDIR)/v342.pre]) unless -f q[$(B_CONFDIR)/v342.pre]"
-  
+	$(PERL) -MFile::Copy -e "copy(q[rules/v343.pre], q[$(B_CONFDIR)/v343.pre]) unless -f q[$(B_CONFDIR)/v343.pre]"
 
 data__install:
 	-$(MKPATH) $(B_DATADIR)

Modified: spamassassin/branches/3.4/UPGRADE
URL: http://svn.apache.org/viewvc/spamassassin/branches/3.4/UPGRADE?rev=1862624&r1=1862623&r2=1862624&view=diff
==============================================================================
--- spamassassin/branches/3.4/UPGRADE (original)
+++ spamassassin/branches/3.4/UPGRADE Fri Jul  5 17:26:47 2019
@@ -1,6 +1,8 @@
 Note for Users Upgrading to SpamAssassin 3.4.3
 ----------------------------------------------
 
+- New OLEMacro plugin to detect OLE Macro inside documents attached to emails
+
 - Due to the dangerous nature of sa-update --allowplugins option, it
   now prints a warning that --reallyallowplugins is required to use it.
   This is to make sure all the legacy installations and wiki guides etc

Added: spamassassin/branches/3.4/lib/Mail/SpamAssassin/Plugin/OLEMacro.pm
URL: http://svn.apache.org/viewvc/spamassassin/branches/3.4/lib/Mail/SpamAssassin/Plugin/OLEMacro.pm?rev=1862624&view=auto
==============================================================================
--- spamassassin/branches/3.4/lib/Mail/SpamAssassin/Plugin/OLEMacro.pm (added)
+++ spamassassin/branches/3.4/lib/Mail/SpamAssassin/Plugin/OLEMacro.pm Fri Jul  5 17:26:47 2019
@@ -0,0 +1,929 @@
+# <@LICENSE>
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to you under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at:
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+# </...@LICENSE>
+
+=head1 NAME
+
+Mail::SpamAssassin::Plugin::OLEMacro - search attached documents for evidence of containing an OLE Macro
+
+=head1 SYNOPSIS
+
+  loadplugin Mail::SpamAssassin::Plugin::OLEMacro
+
+  ifplugin Mail::SpamAssassin::Plugin::OLEMacro
+    body     OLEMACRO eval:check_olemacro()
+    describe OLEMACRO Attachment has an Office Macro
+
+    body     OLEMACRO_MALICE eval:check_olemacro_malice()
+    describe OLEMACRO_MALICE Potentially malicious Office Macro
+
+    body     OLEMACRO_ENCRYPTED eval:check_olemacro_encrypted()
+    describe OLEMACRO_ENCRYPTED Has an Office doc that is encrypted
+
+    body     OLEMACRO_RENAME eval:check_olemacro_renamed()
+    describe OLEMACRO_RENAME Has an Office doc that has been renamed
+
+    body     OLEMACRO_ZIP_PW eval:check_olemacro_zip_password()
+    describe OLEMACRO_ZIP_PW Has an Office doc that is password protected in a zip
+
+    body     OLEMACRO_CSV eval:check_olemacro_csv()
+    describe OLEMACRO_CSV Malicious csv file that tries to exec cmd.exe detected
+  endif
+
+=head1 DESCRIPTION
+
+This plugin detects OLE Macro inside documents attached to emails.
+It can detect documents inside zip files as well as encrypted documents.
+
+=head1 REQUIREMENT
+
+This plugin requires Archive::Zip and IO::String perl modules.
+
+=head1 USER PREFERENCES
+
+The following options can be used in both site-wide (C<local.cf>) and
+user-specific (C<user_prefs>) configuration files to customize how
+the module handles attached documents
+
+=cut
+
+package Mail::SpamAssassin::Plugin::OLEMacro;
+use strict;
+use warnings;
+
+use Mail::SpamAssassin::Plugin;
+use Mail::SpamAssassin::Util qw(compile_regexp);
+
+BEGIN
+{
+    eval{ require Archive::Zip ;
+           import Archive::Zip qw( :ERROR_CODES :CONSTANTS ) } ;
+    eval{ require IO::String ;
+          import  IO::String } ;
+}
+
+use re 'taint';
+
+use vars qw(@ISA);
+@ISA = qw(Mail::SpamAssassin::Plugin);
+
+our $VERSION = '0.52';
+
+# https://www.openoffice.org/sc/compdocfileformat.pdf
+# http://blog.rootshell.be/2015/01/08/searching-for-microsoft-office-files-containing-macro/
+# embedded object in rtf files (https://www.biblioscape.com/rtf15_spec.htm)
+my $marker1 = "\xd0\xcf\x11\xe0";
+my $marker2 = "\x00\x41\x74\x74\x72\x69\x62\x75\x74\x00";
+my $marker3 = "\x5c\x6f\x62\x6a\x65\x6d\x62";
+
+# constructor: register the eval rule
+sub new {
+  my $class = shift;
+  my $mailsaobject = shift;
+
+  # some boilerplate...
+  $class = ref($class) || $class;
+  my $self = $class->SUPER::new($mailsaobject);
+  bless ($self, $class);
+
+  $self->set_config($mailsaobject->{conf});
+
+  $self->register_eval_rule("check_olemacro");
+  $self->register_eval_rule("check_olemacro_csv");
+  $self->register_eval_rule("check_olemacro_malice");
+  $self->register_eval_rule("check_olemacro_renamed");
+  $self->register_eval_rule("check_olemacro_encrypted");
+  $self->register_eval_rule("check_olemacro_zip_password");
+
+  return $self;
+}
+
+sub dbg {
+  Mail::SpamAssassin::Plugin::dbg ("OLEMacro: @_");
+}
+
+sub set_config {
+  my ($self, $conf) = @_;
+  my @cmds = ();
+
+  push(@cmds, {
+    setting => 'olemacro_num_mime',
+    default => 5,
+    type => $Mail::SpamAssassin::Conf::CONF_TYPE_NUMERIC,
+  });
+
+=over 4
+
+=item olemacro_num_mime (default: 5)
+
+Configure the maximum number of matching MIME parts the plugin will scan
+
+=back
+
+=cut
+
+  push(@cmds, {
+    setting => 'olemacro_num_zip',
+    default => 5,
+    type => $Mail::SpamAssassin::Conf::CONF_TYPE_NUMERIC,
+  });
+
+=over 4
+
+=item olemacro_num_zip (default: 5)
+
+Configure the maximum number of matching zip members the plugin will scan
+
+=back
+
+=cut
+
+  push(@cmds, {
+    setting => 'olemacro_zip_depth',
+    default => 2,
+    type => $Mail::SpamAssassin::Conf::CONF_TYPE_NUMERIC,
+  });
+
+=over 4
+
+=item olemacro_zip_depth (default: 2)
+
+Depth to recurse within Zip files
+
+=back
+
+=cut
+
+  push(@cmds, {
+    setting => 'olemacro_extended_scan',
+    default => 0,
+    type => $Mail::SpamAssassin::Conf::CONF_TYPE_BOOL,
+  });
+
+=over 4
+
+=item olemacro_extended_scan ( 0 | 1 ) (default: 0)
+
+Scan more files for potential macros, the C<olemacro_skip_exts> parameter will still be honored
+This parameter is off by default and shouldn't be needed.
+If this is turned on consider adjusting values for C<olemacro_num_mime> and C<olemacro_num_zip> 
+and prepare for more CPU overhead
+
+=back
+
+=cut
+
+  push(@cmds, {
+    setting => 'olemacro_prefer_contentdisposition',
+    default => 1,
+    type => $Mail::SpamAssassin::Conf::CONF_TYPE_BOOL,
+  });
+
+=over 4
+
+=item olemacro_prefer_contentdisposition ( 0 | 1 ) (default: 1)
+
+Choose if the content-disposition header filename be preferred if ambiguity is encountered whilst trying to get filename
+
+=back
+
+=cut
+
+  push(@cmds, {
+    setting => 'olemacro_max_file',
+    default => 512000,
+    type => $Mail::SpamAssassin::Conf::CONF_TYPE_NUMERIC,
+  });
+
+=over 4
+
+=item olemacro_max_file (default: 512000)
+
+Configure the largest file that the plugin will decode from the MIME objects
+
+=back
+
+=cut
+
+  # https://blogs.msdn.microsoft.com/vsofficedeveloper/2008/05/08/office-2007-file-format-mime-types-for-http-content-streaming-2/
+  # https://technet.microsoft.com/en-us/library/ee309278(office.12).aspx
+
+  push(@cmds, {
+    setting => 'olemacro_exts',
+    default => '(?:doc|docx|dot|pot|ppa|pps|ppt|rtf|sldm|xl|xla|xls|xlsx|xlt|xslb)$',
+    type => $Mail::SpamAssassin::Conf::CONF_TYPE_STRING,
+    code => sub {
+      my ($self, $key, $value, $line) = @_;
+      unless (defined $value && $value !~ /^$/) {
+        return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
+      }
+      my ($rec, $err) = compile_regexp($value, 1);
+      if (!$rec) {
+       dbg("config: invalid olemacro_exts '$value': $err");
+       return $Mail::SpamAssassin::Conf::INVALID_VALUE;
+      }
+      $self->{olemacro_exts} = $rec;
+      },
+    }
+  );
+
+=over 4
+
+=item olemacro_exts (default: (?:doc|docx|dot|pot|ppa|pps|ppt|rtf|sldm|xl|xla|xls|xlsx|xlt|xslb)$)
+
+Set the regexp used to configure the extensions the plugin targets for macro scanning
+
+=back
+
+=cut
+
+  push(@cmds, {
+    setting => 'olemacro_macro_exts',
+    default => '(?:docm|dotm|ppam|potm|ppst|ppsm|pptm|sldm|xlm|xlam|xlsb|xlsm|xltm|xps)$',
+    type => $Mail::SpamAssassin::Conf::CONF_TYPE_STRING,
+    code => sub {
+      my ($self, $key, $value, $line) = @_;
+      unless (defined $value && $value !~ /^$/) {
+        return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
+      }
+      my ($rec, $err) = compile_regexp($value, 1);
+      if (!$rec) {
+       dbg("config: invalid olemacro_macro_exts '$value': $err");
+       return $Mail::SpamAssassin::Conf::INVALID_VALUE;
+      }
+      $self->{olemacro_macro_exts} = $rec;
+    },
+  });
+
+=over 4
+
+=item olemacro_macro_exts (default: (?:docm|dotm|ppam|potm|ppst|ppsm|pptm|sldm|xlm|xlam|xlsb|xlsm|xltm|xps)$)
+
+Set the regexp used to configure the extensions the plugin treats as containing a macro
+
+=back
+
+=cut
+
+  push(@cmds, {
+    setting => 'olemacro_skip_exts',
+    default => '(?:dotx|potx|ppsx|pptx|sldx|xltx)$',
+    type => $Mail::SpamAssassin::Conf::CONF_TYPE_STRING,
+    code => sub {
+      my ($self, $key, $value, $line) = @_;
+      unless (defined $value && $value !~ /^$/) {
+        return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
+      }
+      my ($rec, $err) = compile_regexp($value, 1);
+      if (!$rec) {
+       dbg("config: invalid olemacro_skip_exts '$value': $err");
+       return $Mail::SpamAssassin::Conf::INVALID_VALUE;
+      }
+
+      $self->{olemacro_skip_exts} = $rec;
+    },
+  });
+
+=over 4
+
+=item olemacro_skip_exts (default: (?:dotx|potx|ppsx|pptx|sldx|xltx)$)
+
+Set the regexp used to configure extensions for the plugin to skip entirely, these should only be guaranteed macro free files
+
+=back
+
+=cut
+
+  push(@cmds, {
+    setting => 'olemacro_skip_ctypes',
+    default => '^(?:(audio|image|text)\/|application\/(?:pdf))',
+    type => $Mail::SpamAssassin::Conf::CONF_TYPE_STRING,
+    code => sub {
+      my ($self, $key, $value, $line) = @_;
+      unless (defined $value && $value !~ /^$/) {
+        return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
+      }
+      my ($rec, $err) = compile_regexp($value, 1);
+      if (!$rec) {
+       dbg("config: invalid olemacro_skip_ctypes '$value': $err");
+       return $Mail::SpamAssassin::Conf::INVALID_VALUE;
+      }
+
+      $self->{olemacro_skip_ctypes} = $rec;
+    },
+  });
+
+=over 4
+
+=item olemacro_skip_ctypes (default: ^(?:(audio|image|text)\/|application\/(?:pdf)))
+
+Set the regexp used to configure content types for the plugin to skip entirely, these should only be guaranteed macro free
+
+=back
+
+=cut
+
+  push(@cmds, {
+    setting => 'olemacro_zips',
+    default => '(?:zip)$',
+    type => $Mail::SpamAssassin::Conf::CONF_TYPE_STRING,
+    code => sub {
+      my ($self, $key, $value, $line) = @_;
+      unless (defined $value && $value !~ /^$/) {
+        return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
+      }
+      my ($rec, $err) = compile_regexp($value, 1);
+      if (!$rec) {
+       dbg("config: invalid olemacro_zips '$value': $err");
+       return $Mail::SpamAssassin::Conf::INVALID_VALUE;
+      }
+
+      $self->{olemacro_zips} = $rec;
+    },
+  });
+
+=over 4
+
+=item olemacro_zips (default: (?:zip)$)
+
+Set the regexp used to configure extensions for the plugin to target as zip files, 
+files listed in configs above are also tested for zip
+
+=back
+
+=cut
+
+  $conf->{parser}->register_commands(\@cmds);
+}
+
+sub check_olemacro {
+  my ($self,$pms,$body,$name) = @_;
+
+  _check_attachments(@_) unless exists $pms->{olemacro_exists};
+
+  return $pms->{olemacro_exists};
+}
+
+sub check_olemacro_csv {
+  my ($self,$pms,$body,$name) = @_;
+
+  my $chunk_size = $pms->{conf}->{olemacro_max_file};
+
+  foreach my $part ($pms->{msg}->find_parts(qr/./, 1)) {
+
+    next unless ($part->{type} eq "text/plain");
+
+    my ($ctt, $ctd, $cte, $name) = _get_part_details($pms, $part);
+    next unless defined $ctt;
+
+    next if $name eq '';
+
+    # we skipped what we need/want to
+    my $data = undef;
+
+    # if name extension is csv - return true
+    if ($name =~ /\.csv/i) {
+      dbg("Found csv file with name $name");
+      $data = $part->decode($chunk_size) unless defined $data;
+      if($data =~ /MSEXCEL|.*Windows\\System32\\cmd\\.exe/) {
+        $pms->{olemacro_csv} = 1;
+      }
+    }
+  }
+  return $pms->{olemacro_csv};
+}
+
+sub check_olemacro_malice {
+  my ($self,$pms,$body,$name) = @_;
+
+  _check_attachments(@_) unless exists $pms->{olemacro_malice};
+
+  return $pms->{olemacro_malice};
+}
+
+sub check_olemacro_renamed {
+  my ($self,$pms,$body,$name) = @_;
+
+  _check_attachments(@_) unless exists $pms->{olemacro_renamed};
+
+  return $pms->{olemacro_renamed};
+}
+
+sub check_olemacro_encrypted {
+  my ($self,$pms,$body,$name) = @_;
+
+  _check_attachments(@_) unless exists $pms->{olemacro_encrypted};
+
+  return $pms->{olemacro_encrypted};
+}
+
+sub check_olemacro_zip_password {
+  my ($self,$pms,$body,$name) = @_;
+
+  _check_attachments(@_) unless exists $pms->{olemacro_zip_password};
+
+  return $pms->{olemacro_zip_password};
+}
+
+sub _check_attachments {
+
+  my ($self,$pms,$body,$name) = @_;
+
+  my $mimec = 0;
+  my $chunk_size = $pms->{conf}->{olemacro_max_file};
+
+  $pms->{olemacro_exists} = 0;
+  $pms->{olemacro_malice} = 0;
+  $pms->{olemacro_renamed} = 0;
+  $pms->{olemacro_encrypted} = 0;
+  $pms->{olemacro_zip_password} = 0;
+  $pms->{olemacro_office_xml} = 0;
+
+  foreach my $part ($pms->{msg}->find_parts(qr/./, 1)) {
+
+    next if ($part->{type} =~ $pms->{conf}->{olemacro_skip_ctypes});
+
+    my ($ctt, $ctd, $cte, $name) = _get_part_details($pms, $part);
+    next unless defined $ctt;
+
+    next if $name eq '';
+    next if (lc($name) =~ $pms->{conf}->{olemacro_skip_exts});
+
+    # we skipped what we need/want to
+    my $data = undef;
+
+    # if name is macrotype - return true
+    if (lc($name) =~ $pms->{conf}->{olemacro_macro_exts}) {
+      dbg("Found macrotype attachment with name $name");
+      $pms->{olemacro_exists} = 1;
+
+      $data = $part->decode($chunk_size) unless defined $data;
+
+      _check_encrypted_doc($pms, $name, $data);
+      _check_macrotype_doc($pms, $name, $data);
+
+      return 1 if $pms->{olemacro_exists} == 1;
+    }
+
+    # if name is ext type - check and return true if needed
+    if (lc($name) =~ $pms->{conf}->{olemacro_exts}) {
+      dbg("Found attachment with name $name");
+      $data = $part->decode($chunk_size) unless defined $data;
+
+      _check_encrypted_doc($pms, $name, $data);
+      _check_oldtype_doc($pms, $name, $data);
+      # zipped doc that matches olemacro_exts - strange
+      if (_check_macrotype_doc($pms, $name, $data)) {
+        $pms->{olemacro_renamed} = $pms->{olemacro_office_xml};
+      }
+
+      return 1 if $pms->{olemacro_exists} == 1;
+    }
+
+    if (lc($name) =~ $pms->{conf}->{olemacro_zips}) {
+      dbg("Found zip attachment with name $name");
+      $data = $part->decode($chunk_size) unless defined $data;
+
+      _check_zip($pms, $name, $data);
+
+      return 1 if $pms->{olemacro_exists} == 1;
+    }
+
+    if ($pms->{conf}->{olemacro_extended_scan} == 1) {
+      dbg("Extended scan attachment with name $name");
+      $data = $part->decode($chunk_size) unless defined $data;
+
+      if (_is_office_doc($data)) {
+        $pms->{olemacro_renamed} = 1;
+        dbg("Found $name to be an Office Doc!");
+        _check_encrypted_doc($pms, $name, $data);
+        _check_oldtype_doc($pms, $name, $data);
+      }
+
+      if (_check_macrotype_doc($pms, $name, $data)) {
+        $pms->{olemacro_renamed} = $pms->{olemacro_office_xml};
+      }
+
+      _check_zip($pms, $name, $data);
+
+      return 1 if $pms->{olemacro_exists} == 1;
+    }
+
+    # if we get to here with data a part has been scanned nudge as reqd
+    $mimec+=1 if defined $data;
+    if ($mimec >= $pms->{conf}->{olemacro_num_mime}) {
+      dbg('MIME limit reached');
+      last;
+    }
+
+  }
+  return 0;
+}
+
+sub _check_zip {
+  my ($pms, $name, $data, $depth) = @_;
+
+  return 0 if $pms->{conf}->{olemacro_num_zip} == 0;
+
+  $depth = $depth || 1;
+  return 0 if ($depth > $pms->{conf}->{olemacro_zip_depth});
+
+  return 0 unless _is_zip_file($name, $data);
+  my $zip = _open_zip_handle($data);
+  return 0 unless $zip;
+
+  dbg("Zip opened");
+
+  my $filec = 0;
+  my @members = $zip->members();
+  # foreach zip member
+  # - skip if in skip exts
+  # - return 1 if in macro types
+  # - check for marker if doc type
+  # - check if a zip
+  foreach my $member (@members){
+    my $mname = lc $member->fileName();
+    next if ($mname =~ $pms->{conf}->{olemacro_skip_exts});
+
+    my $data = undef;
+    my $status = undef;
+
+    # if name is macrotype - return true
+    if ($mname =~ $pms->{conf}->{olemacro_macro_exts}) {
+      dbg("Found macrotype zip member $mname");
+      $pms->{olemacro_exists} = 1;
+
+      if ($member->isEncrypted()) {
+        dbg("Zip member $mname is encrypted (zip pw)");
+        $pms->{olemacro_zip_password} = 1;
+        return 1;
+      }
+
+      ( $data, $status ) = $member->contents() unless defined $data;
+      return 1 unless $status == AZ_OK;
+
+      _check_encrypted_doc($pms, $name, $data);
+      _check_macrotype_doc($pms, $name, $data);
+
+      return 1 if $pms->{olemacro_exists} == 1;
+    }
+
+    if ($mname =~ $pms->{conf}->{olemacro_exts}) {
+      dbg("Found zip member $mname");
+
+      if ($member->isEncrypted()) {
+        dbg("Zip member $mname is encrypted (zip pw)");
+        $pms->{olemacro_zip_password} = 1;
+        next;
+      }
+
+      ( $data, $status ) = $member->contents() unless defined $data;
+      next unless $status == AZ_OK;
+
+
+      _check_encrypted_doc($pms, $name, $data);
+      _check_oldtype_doc($pms, $name, $data);
+      # zipped doc that matches olemacro_exts - strange
+      if (_check_macrotype_doc($pms, $name, $data)) {
+        $pms->{olemacro_renamed} = $pms->{olemacro_office_xml};
+      }
+
+      return 1 if $pms->{olemacro_exists} == 1;
+
+    }
+
+    if ($mname =~ $pms->{conf}->{olemacro_zips}) {
+      dbg("Found zippy zip member $mname");
+      ( $data, $status ) = $member->contents() unless defined $data;
+      next unless $status == AZ_OK;
+
+      _check_zip($pms, $name, $data, $depth);
+
+      return 1 if $pms->{olemacro_exists} == 1;
+
+    }
+
+    if ($pms->{conf}->{olemacro_extended_scan} == 1) {
+      dbg("Extended scan attachment with member name $mname");
+      ( $data, $status ) = $member->contents() unless defined $data;
+      next unless $status == AZ_OK;
+
+      if (_is_office_doc($data)) {
+        dbg("Found $name to be an Office Doc!");
+        _check_encrypted_doc($pms, $name, $data);
+        $pms->{olemacro_renamed} = 1;
+        _check_oldtype_doc($pms, $name, $data);
+      }
+
+      if (_check_macrotype_doc($pms, $name, $data)) {
+        $pms->{olemacro_renamed} = $pms->{olemacro_office_xml};
+      }
+
+      _check_zip($pms, $name, $data, $depth);
+
+      return 1 if $pms->{olemacro_exists} == 1;
+
+    }
+
+    # if we get to here with data a member has been scanned nudge as reqd
+    $filec+=1 if defined $data;
+    if ($filec >= $pms->{conf}->{olemacro_num_zip}) {
+      dbg('Zip limit reached');
+      last;
+    }
+  }
+  return 0;
+}
+
+sub _get_part_details {
+    my ($pms, $part) = @_;
+    #https://en.wikipedia.org/wiki/MIME#Content-Disposition
+    #https://github.com/mikel/mail/pull/464
+
+    my $ctt = $part->get_header('content-type');
+    return undef unless defined $ctt; ## no critic (ProhibitExplicitReturnUndef)
+
+    my $cte = lc($part->get_header('content-transfer-encoding') || '');
+    return undef unless ($cte =~ /^(?:base64|quoted\-printable)$/); ## no critic (ProhibitExplicitReturnUndef)
+
+    $ctt = _decode_part_header($part, $ctt || '');
+
+    my $name = '';
+    my $cttname = '';
+    my $ctdname = '';
+
+    if($ctt =~ m/(?:file)?name\s*=\s*["']?([^"';]*)["']?/is){
+      $cttname = $1;
+      $cttname =~ s/\s+$//;
+    }
+
+    my $ctd = $part->get_header('content-disposition');
+    $ctd = _decode_part_header($part, $ctd || '');
+
+    if($ctd =~ m/filename\s*=\s*["']?([^"';]*)["']?/is){
+      $ctdname = $1;
+      $ctdname =~ s/\s+$//;
+    }
+
+    if (lc $ctdname eq lc $cttname) {
+      $name = $ctdname;
+    } elsif ($ctdname eq '') {
+      $name = $cttname;
+    } elsif ($cttname eq '') {
+      $name = $ctdname;
+    } else {
+      if ($pms->{conf}->{olemacro_prefer_contentdisposition}) {
+        $name = $ctdname;
+      } else {
+        $name = $cttname;
+      }
+    }
+
+    return $ctt, $ctd, $cte, lc $name;
+}
+
+sub _open_zip_handle {
+  my ($data) = @_;
+  # open our archive from raw datas
+  my $SH = IO::String->new($data);
+
+  Archive::Zip::setErrorHandler( \&_zip_error_handler );
+  my $zip = Archive::Zip->new();
+  if($zip->readFromFileHandle( $SH ) != AZ_OK){
+    dbg("cannot read zipfile");
+    # as we cannot read it its not a zip (or too big/corrupted)
+    # so skip processing.
+    return 0;
+  }
+  return $zip;
+}
+
+sub _check_macrotype_doc {
+  my ($pms, $name, $data) = @_;
+
+  return 0 unless _is_zip_file($name, $data);
+
+  my $zip = _open_zip_handle($data);
+  return 0 unless $zip;
+
+  #https://www.decalage.info/vba_tools
+  my %macrofiles = (
+    'word/vbaproject.bin' => 'word2k7',
+    'macros/vba/_vba_project' => 'word97',
+    'xl/vbaproject.bin' => 'xl2k7',
+    '_vba_project_cur/vba/_vba_project' => 'xl97',
+    'ppt/vbaproject.bin' => 'ppt2k7',
+  );
+
+  my @members = $zip->members();
+  foreach my $member (@members){
+    my $mname = lc $member->fileName();
+    if (exists($macrofiles{$mname})) {
+      dbg("Found $macrofiles{$mname} vba file");
+      $pms->{olemacro_exists} = 1;
+      last;
+    }
+  }
+
+  # Look for a member named [Content_Types].xml and do checks
+  if (my $ctypesxml = $zip->memberNamed('[Content_Types].xml')) {
+    dbg('Found [Content_Types].xml file');
+    $pms->{olemacro_office_xml} = 1;
+    if (!$pms->{olemacro_exists}) {
+      my ( $data, $status ) = $ctypesxml->contents();
+
+      if (($status == AZ_OK) && (_check_ctype_xml($data))) {
+        $pms->{olemacro_exists} = 1;
+      }
+    }
+  }
+
+  if (($pms->{olemacro_exists}) && (_find_malice_bins($zip))) {
+    $pms->{olemacro_malice} = 1;
+  }
+
+  return $pms->{olemacro_exists};
+
+}
+
+# Office 2003
+
+sub _check_oldtype_doc {
+  my ($pms, $name, $data) = @_;
+
+  if (_check_markers($data)) {
+    $pms->{olemacro_exists} = 1;
+    if (_check_malice($data)) {
+     $pms->{olemacro_malice} = 1;
+    }
+    return 1;
+  }
+}
+
+# Encrypted doc
+
+sub _check_encrypted_doc {
+  my ($pms, $name, $data) = @_;
+
+  if (_is_encrypted_doc($data)) {
+    dbg("File $name is encrypted");
+    $pms->{olemacro_encrypted} = 1;
+  }
+
+  return $pms->{olemacro_encrypted};
+}
+
+sub _is_encrypted_doc {
+  my ($data) = @_;
+
+  #http://stackoverflow.com/questions/14347513/how-to-detect-if-a-word-document-is-password-protected-before-uploading-the-file/14347730#14347730
+  if (_is_office_doc($data)) {
+    if ($data =~ /(?:<encryption xmlns)/i) {
+      return 1;
+    }
+    if (index($data, "\x13") == 523) {
+      return 1;
+    }
+    if (index($data, "\x2f") == 532) {
+      return 1;
+    }
+    if (index($data, "\xfe") == 520) {
+      return 1;
+    }
+    my $tdata = substr $data, 2000;
+    $tdata =~ s/\\0/ /g;
+    if (index($tdata, "E n c r y p t e d P a c k a g e") > -1) {
+      return 1;
+    }
+  }
+}
+
+sub _is_office_doc {
+  my ($data) = @_;
+  if (index($data, $marker1) == 0) {
+    return 1;
+  }
+}
+
+sub _is_zip_file {
+  my ($name, $data) = @_;
+  if (index($data, 'PK') == 0) {
+    return 1;
+  } else {
+    return($name =~ /(?:zip)$/);
+  }
+}
+
+sub _check_markers {
+  my ($data) = @_;
+
+  if (index($data, $marker1) == 0 && index($data, $marker2) > -1) {
+    dbg('Marker found');
+    return 1;
+  }
+
+  if (index($data, $marker3) > -1) {
+    dbg('Marker found');
+    return 1;
+  }
+
+  if (index($data, 'w:macrosPresent="yes"') > -1) {
+    dbg('XML macros marker found');
+    return 1;
+  }
+
+  if (index($data, 'vbaProject.bin.rels') > -1) {
+    dbg('XML macros marker found');
+    return 1;
+  }
+}
+
+sub _find_malice_bins {
+  my ($zip) = @_;
+
+  my @binfiles = $zip->membersMatching( '.*\.bin' );
+
+  foreach my $member (@binfiles){
+    my ( $data, $status ) = $member->contents();
+    next unless $status == AZ_OK;
+    if (_check_malice($data)) {
+      return 1;
+    }
+  }
+}
+
+sub _check_malice {
+  my ($data) = @_;
+
+  # https://www.greyhathacker.net/?p=872
+  if ($data =~ /(?:document|auto|workbook)_?open/i) {
+    dbg('Found potential malicious code');
+    return 1;
+  }
+}
+
+sub _check_ctype_xml {
+  my ($data) = @_;
+
+  # http://download.microsoft.com/download/D/3/3/D334A189-E51B-47FF-B0E8-C0479AFB0E3C/[MS-OFFMACRO].pdf
+  if ($data =~ /ContentType=["']application\/vnd\.ms-office\.vbaProject["']/i){
+    dbg('Found VBA ref');
+    return 1;
+  }
+  if ($data =~ /macroEnabled/i) {
+    dbg('Found Macro Ref');
+    return 1;
+  }
+  if ($data =~ /application\/vnd\.ms-excel\.(?:intl)?macrosheet/i) {
+    dbg('Excel macrosheet found');
+    return 1;
+  }
+}
+
+sub _zip_error_handler {
+ 1;
+}
+
+sub _decode_part_header {
+  my($part, $header_field_body) = @_;
+
+  return '' unless defined $header_field_body && $header_field_body ne '';
+
+  # deal with folding and cream the newlines and such
+  $header_field_body =~ s/\n[ \t]+/\n /g;
+  $header_field_body =~ s/\015?\012//gs;
+
+  local($1,$2,$3);
+
+  # Multiple encoded sections must ignore the interim whitespace.
+  # To avoid possible FPs with (\s+(?==\?))?, look for the whole RE
+  # separated by whitespace.
+  1 while $header_field_body =~
+            s{ ( = \? [A-Za-z0-9_-]+ \? [bqBQ] \? [^?]* \? = ) \s+
+               ( = \? [A-Za-z0-9_-]+ \? [bqBQ] \? [^?]* \? = ) }
+             {$1$2}xsg;
+
+  # transcode properly encoded RFC 2047 substrings into UTF-8 octets,
+  # leave everything else unchanged as it is supposed to be UTF-8 (RFC 6532)
+  # or plain US-ASCII
+  $header_field_body =~
+    s{ (?: = \? ([A-Za-z0-9_-]+) \? ([bqBQ]) \? ([^?]*) \? = ) }
+     { $part->__decode_header($1, uc($2), $3) }xsge;
+
+  return $header_field_body;
+}
+
+1;

Modified: spamassassin/trunk/MANIFEST
URL: http://svn.apache.org/viewvc/spamassassin/trunk/MANIFEST?rev=1862624&r1=1862623&r2=1862624&view=diff
==============================================================================
--- spamassassin/trunk/MANIFEST (original)
+++ spamassassin/trunk/MANIFEST Fri Jul  5 17:26:47 2019
@@ -147,7 +147,7 @@ rules/v330.pre
 rules/v340.pre
 rules/v341.pre
 rules/v342.pre
-rules/v400.pre
+rules/v343.pre
 rules/20_aux_tlds.cf
 rules-extras/README.txt
 rules-extras/10_uridnsbl_skip_financial.cf

Modified: spamassassin/trunk/Makefile.PL
URL: http://svn.apache.org/viewvc/spamassassin/trunk/Makefile.PL?rev=1862624&r1=1862623&r2=1862624&view=diff
==============================================================================
--- spamassassin/trunk/Makefile.PL (original)
+++ spamassassin/trunk/Makefile.PL Fri Jul  5 17:26:47 2019
@@ -1132,8 +1132,7 @@ conf__install:
 	$(PERL) -MFile::Copy -e "copy(q[rules/v340.pre], q[$(B_CONFDIR)/v340.pre]) unless -f q[$(B_CONFDIR)/v340.pre]"
 	$(PERL) -MFile::Copy -e "copy(q[rules/v341.pre], q[$(B_CONFDIR)/v341.pre]) unless -f q[$(B_CONFDIR)/v341.pre]"
 	$(PERL) -MFile::Copy -e "copy(q[rules/v342.pre], q[$(B_CONFDIR)/v342.pre]) unless -f q[$(B_CONFDIR)/v342.pre]"
-	$(PERL) -MFile::Copy -e "copy(q[rules/v400.pre], q[$(B_CONFDIR)/v400.pre]) unless -f q[$(B_CONFDIR)/v400.pre]"
-  
+	$(PERL) -MFile::Copy -e "copy(q[rules/v343.pre], q[$(B_CONFDIR)/v343.pre]) unless -f q[$(B_CONFDIR)/v343.pre]"
 
 data__install:
 	-$(MKPATH) $(B_DATADIR)

Added: spamassassin/trunk/rules/v343.pre
URL: http://svn.apache.org/viewvc/spamassassin/trunk/rules/v343.pre?rev=1862624&view=auto
==============================================================================
--- spamassassin/trunk/rules/v343.pre (added)
+++ spamassassin/trunk/rules/v343.pre Fri Jul  5 17:26:47 2019
@@ -0,0 +1,20 @@
+# This is the right place to customize your installation of SpamAssassin.
+#
+# See 'perldoc Mail::SpamAssassin::Conf' for details of what can be
+# tweaked.
+#
+# This file was installed during the installation of SpamAssassin 4.0.0,
+# and contains plugin loading commands for the new plugins added in that
+# release.  It will not be overwritten during future SpamAssassin installs,
+# so you can modify it to enable some disabled-by-default plugins below,
+# if you so wish.
+#
+# There are now multiple files read to enable plugins in the
+# /etc/mail/spamassassin directory; previously only one, "init.pre" was
+# read.  Now both "init.pre", "v310.pre", and any other files ending in
+# ".pre" will be read.  As future releases are made, new plugins will be
+# added to new files, named according to the release they're added in.
+###########################################################################
+
+# OLEMacro - Detects macros in Office documents
+# loadplugin Mail::SpamAssassin::Plugin::OLEMacro