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');