svn commit: r1936167 - in spamassassin/trunk: . lib/Mail lib/Mail/SpamAssassin t t/data/nice

[email protected] Wed, 15 Jul 2026 17:51:21 -0000
Newsgroups gmane.mail.spam.spamassassin.cvs
Message-ID <178413788165.2095828.10929501783203957506@svn03-he-fi>
Author: fkento
Date: Wed Jul 15 17:51:21 2026
New Revision: 1936167

Log:
Bug 6439: Scan text attachments

Added:
   spamassassin/trunk/t/data/nice/scan_text_attach
   spamassassin/trunk/t/scan_text_attach.t
Modified:
   spamassassin/trunk/MANIFEST
   spamassassin/trunk/UPGRADE
   spamassassin/trunk/lib/Mail/SpamAssassin.pm
   spamassassin/trunk/lib/Mail/SpamAssassin/Conf.pm
   spamassassin/trunk/lib/Mail/SpamAssassin/Message.pm

Modified: spamassassin/trunk/MANIFEST
==============================================================================
--- spamassassin/trunk/MANIFEST	Wed Jul 15 15:42:52 2026	(r1936166)
+++ spamassassin/trunk/MANIFEST	Wed Jul 15 17:51:21 2026	(r1936167)
@@ -453,6 +453,7 @@ t/data/nice/mime9
 t/data/nice/no_body
 t/data/nice/not_gtube.eml
 t/data/nice/orig_ip_hdr.eml
+t/data/nice/scan_text_attach
 t/data/nice/spf1
 t/data/nice/spf2
 t/data/nice/spf3
@@ -730,6 +731,7 @@ t/sa_compile.t
 t/sa_txrep.t
 t/sa_txrep_sql.t
 t/sa_txrep_welcomelist_out.t
+t/scan_text_attach.t
 t/sha1.t
 t/shortcircuit.t
 t/shortcircuit_before_dns.t

Modified: spamassassin/trunk/UPGRADE
==============================================================================
--- spamassassin/trunk/UPGRADE	Wed Jul 15 15:42:52 2026	(r1936166)
+++ spamassassin/trunk/UPGRADE	Wed Jul 15 17:51:21 2026	(r1936167)
@@ -56,6 +56,13 @@ Note for Users Upgrading to SpamAssassin
   skipping the other alternative. By default this is unset and all parts
   are included as before.
 
+- New configuration option: scan_text_attachments
+  By default, text/plain and text/html parts marked with
+  "Content-Disposition: attachment" are excluded from the rendered body
+  text that "body" rules run against. When this option is enabled, such
+  attachments are rendered and included like any other part so body
+  rules can inspect them. Default is 0 (off) to match existing behaviour.
+
 - New Mail::SpamAssassin::Plugin::NeuralNetwork
   This plugin checks messages using Fast Artificial Neural Network library.
 

Modified: spamassassin/trunk/lib/Mail/SpamAssassin.pm
==============================================================================
--- spamassassin/trunk/lib/Mail/SpamAssassin.pm	Wed Jul 15 15:42:52 2026	(r1936166)
+++ spamassassin/trunk/lib/Mail/SpamAssassin.pm	Wed Jul 15 17:51:21 2026	(r1936167)
@@ -622,6 +622,7 @@ sub parse {
     body_part_scan_size=>$self->{conf}->{body_part_scan_size},
     rawbody_part_scan_size=>$self->{conf}->{rawbody_part_scan_size},
     multipart_alternative_preferred_part=>$self->{conf}->{multipart_alternative_preferred_part},
+    scan_text_attachments=>$self->{conf}->{scan_text_attachments},
     master_deadline=>$master_deadline, suppl_attrib=>$suppl_attrib });
 
   # bug 5069: The goal here is to get rendering plugins to do things

Modified: spamassassin/trunk/lib/Mail/SpamAssassin/Conf.pm
==============================================================================
--- spamassassin/trunk/lib/Mail/SpamAssassin/Conf.pm	Wed Jul 15 15:42:52 2026	(r1936166)
+++ spamassassin/trunk/lib/Mail/SpamAssassin/Conf.pm	Wed Jul 15 17:51:21 2026	(r1936167)
@@ -3837,6 +3837,27 @@ empty (the default), all parts are inclu
     }
   });
 
+=item scan_text_attachments               (default: 0 (off))
+
+By default, C<text/*> mime-parts marked with C<Content-Disposition: attachment>
+are excluded from the rendered body text that "body" rules run against, on the
+assumption that a genuine text attachment (a log file, an exported document)
+is not part of the message body.  However, phishing messages routinely deliver
+their payload as an attached C<text/html> (or C<text/plain>) file precisely to
+evade body scanning.
+
+When this option is enabled, text attachments are rendered and included in the
+body text like any other part, so body rules can inspect their content.
+
+=cut
+
+  push (@cmds, {
+    setting => 'scan_text_attachments',
+    is_admin => 1,
+    default => 0,
+    type => $CONF_TYPE_BOOL,
+  });
+
 =item rbl_timeout t [t_min] [zone]		(default: 15 3)
 
 All DNS queries are made at the beginning of a check and we try to read

Modified: spamassassin/trunk/lib/Mail/SpamAssassin/Message.pm
==============================================================================
--- spamassassin/trunk/lib/Mail/SpamAssassin/Message.pm	Wed Jul 15 15:42:52 2026	(r1936166)
+++ spamassassin/trunk/lib/Mail/SpamAssassin/Message.pm	Wed Jul 15 17:51:21 2026	(r1936167)
@@ -122,6 +122,7 @@ sub new {
   $self->{body_part_scan_size} = $opts->{'body_part_scan_size'} || 0;
   $self->{rawbody_part_scan_size} = $opts->{'rawbody_part_scan_size'} || 0;
   $self->{multipart_alternative_preferred_part} = $opts->{'multipart_alternative_preferred_part'} || '';
+  $self->{scan_text_attachments} = $opts->{'scan_text_attachments'} || 0;
 
   if ($self->{suppl_attrib}) {  # caller-provided additional information
     # pristine_body_length is currently used by an eval test check_body_length
@@ -738,6 +739,7 @@ sub finish {
   delete $self->{'body_part_scan_size'};
   delete $self->{'rawbody_part_scan_size'};
   delete $self->{'multipart_alternative_preferred_part'};
+  delete $self->{'scan_text_attachments'};
   delete $self->{'pristine_msg'};
   delete $self->{'pristine_body'};
   delete $self->{'pristine_headers'};
@@ -1434,9 +1436,11 @@ sub get_body_text_array_common {
     my($type, $rnd) = $p->$method_name();  # decode this part
     # Only text/* types are rendered ...
     if (defined $rnd) {
-      # Skip text attachments that are not considered part of the email body
+      # Skip text attachments that are not considered part of the email body,
+      # unless scan_text_attachments is enabled (e.g. to catch phishing payloads
+      # delivered as attached text/html or text/plain files). 
       my $cdisp = $p->{'headers'}->{'content-disposition'}[0];
-      if(($method_name ne 'invisible_rendered') and (defined $cdisp and ($cdisp =~ /^attachment;/) and ($type =~ /text\//))) {
+      if(!$self->{scan_text_attachments} and (defined $cdisp and ($cdisp =~ /^attachment;/) and ($type =~ m{^text/(?:plain|html)$}))) {
         dbg("$method_name: Skipping text attachment with content-disposition \"$cdisp\"");
         next;
       }

Added: spamassassin/trunk/t/data/nice/scan_text_attach
==============================================================================
--- /dev/null	00:00:00 1970	(empty, because file is newly added)
+++ spamassassin/trunk/t/data/nice/scan_text_attach	Wed Jul 15 17:51:21 2026	(r1936167)
@@ -0,0 +1,21 @@
+From: [email protected]
+To: [email protected]
+Subject: Direct Deposit Remittance Advice
+Message-ID: <[email protected]>
+MIME-Version: 1.0
+Content-Type: multipart/mixed; boundary="OUTER"
+
+--OUTER
+Content-Type: text/plain; charset="us-ascii"
+
+Please see the attached remittance advice.
+--OUTER
+Content-Type: text/html; name="Remittance Advice.html"
+Content-Disposition: attachment; filename="Remittance Advice.html"
+
+<html><body>
+<h1>Bank of America</h1>
+<p>UNIQUEPHISHMARKER open the attachment and follow the instructions.</p>
+<p style="display:none">UNIQUEHIDDENMARKER hidden stuffing</p>
+</body></html>
+--OUTER--

Added: spamassassin/trunk/t/scan_text_attach.t
==============================================================================
--- /dev/null	00:00:00 1970	(empty, because file is newly added)
+++ spamassassin/trunk/t/scan_text_attach.t	Wed Jul 15 17:51:21 2026	(r1936167)
@@ -0,0 +1,66 @@
+#!/usr/bin/perl -w -T
+
+use strict;
+use lib '.'; use lib 't';
+use SATest; sa_t_init("scan_text_attach");
+use Test::More tests => 8;
+
+use Mail::SpamAssassin;
+
+# A text/html part marked Content-Disposition: attachment is, by default,
+# excluded from the rendered body text that body rules see.  The
+# scan_text_attachments option includes it.  See t/data/nice/scan_text_attach.
+#
+# scan_text_attachments gates all three rendered arrays uniformly (rendered,
+# visible, invisible): whether a text attachment participates at all is this
+# option's job.  Which streams Bayes tokenizes is bayes_token_sources' job --
+# the two are orthogonal, so with the flag off the attachment's *invisible*
+# text is excluded too (not just its visible text).
+
+my $vis_marker = 'UNIQUEPHISHMARKER';    # visible attachment text
+my $inv_marker = 'UNIQUEHIDDENMARKER';   # display:none attachment text
+
+sub bodies {
+  my ($post_config_text) = @_;
+  my $sa = create_saobj({
+    dont_copy_prefs => 1,
+    post_config_text => $post_config_text,
+  });
+  $sa->init(0);
+  open (IN, "<data/nice/scan_text_attach") or die "cannot open fixture: $!";
+  my $mail = $sa->parse(\*IN);
+  close IN;
+  my %b = (
+    rendered  => join('||', @{$mail->get_rendered_body_text_array()}),
+    invisible => join('||', @{$mail->get_invisible_rendered_body_text_array()}),
+  );
+  $mail->finish;
+  $sa->finish;
+  return %b;
+}
+
+# ---------------------------------------------------------------------------
+# Default: attachment text is excluded from ALL arrays, including invisible
+
+my %off = bodies('');
+unlike($off{rendered}, qr/\Q$vis_marker\E/,
+  'attachment visible text excluded from body by default');
+like($off{rendered}, qr/remittance advice/i,
+  'inline text/plain body part still present by default');
+unlike($off{invisible}, qr/\Q$inv_marker\E/,
+  'attachment invisible text excluded from invisible array by default');
+
+# ---------------------------------------------------------------------------
+# scan_text_attachments 1: attachment text is included in all arrays
+
+my %on = bodies('scan_text_attachments 1');
+like($on{rendered}, qr/\Q$vis_marker\E/,
+  'attachment visible text included in body with scan_text_attachments 1');
+like($on{rendered}, qr/remittance advice/i,
+  'inline text/plain body part still present with scan_text_attachments 1');
+like($on{rendered}, qr/\Q$inv_marker\E/,
+  'attachment invisible text folded into body with scan_text_attachments 1');
+like($on{invisible}, qr/\Q$inv_marker\E/,
+  'attachment invisible text included in invisible array with scan_text_attachments 1');
+unlike($on{invisible}, qr/\Q$vis_marker\E/,
+  'visible text does not leak into the invisible array');