From 132b8145199aa5079269f9c1e0713117886f872e Mon Sep 17 00:00:00 2001
From: Ben Wong <ben@wongs.net>
Date: Sun, 5 Jul 2026 16:28:16 -0700
Subject: [PATCH 1/3] Test if user email addresses might be interpreted as
 sendmail flags

It is legal to have an email address that begins with a dash ('-'),
but when presented to sendmail it may be parsed as a switch that
controls how the program works instead of as a recipient address.
(Exim works the same way. Qmail never takes arguments and is
unaffected).

Please see CWE-88: Improper Neutralization of Argument Delimiters in a
Command ('Argument Injection').

For comparison of a similar vulnerability, see CVE-2016-10033.
---
 t/24_recipient_injection.t | 170 +++++++++++++++++++++++++++++++++++++
 1 file changed, 170 insertions(+)
 create mode 100644 t/24_recipient_injection.t

diff --git a/t/24_recipient_injection.t b/t/24_recipient_injection.t
new file mode 100644
index 0000000..81bc0bf
--- /dev/null
+++ b/t/24_recipient_injection.t
@@ -0,0 +1,170 @@
+# -*- mode: cperl;-*-
+
+use Test::More tests=>3;
+
+use warnings;
+use strict;
+
+# This test checks whether an attacker-controlled From header can
+# reach the sendmail* invocation as a literal, unfiltered argument.
+# (Note: "sendmail" is a stand-in for any supported MTA:
+# sendmail/exim/qmail.)
+
+use IO::File;
+use File::Temp qw(tempdir);
+use Cwd qw(getcwd);
+use File::Basename qw(dirname basename);
+use lib qw(t/lib);
+use DebbugsTest qw(:all);
+
+$SIG{CHLD} = sub {};
+
+my %config = create_debbugs_configuration();
+my $sendmail_dir = $config{sendmail_dir};
+
+# from_email begins with '-' the way a hostile From local-part could;
+# it's syntactically legal per RFC 5322 atext, so nothing upstream
+# rejects it as "not an email address".
+my $from_email = '-oQ/tmp/should-not-exist-marker@example.com';
+
+my $test_name = '';
+
+# Create a new bug report to verify initial auto reply.
+$test_name = 'Initial autoreply to a bug submission';
+send_message(to      => 'submit@bugs.something',
+	     headers => [To      => 'submit@bugs.something',
+			 From    => $from_email,
+			 Subject => 'test bug for injection check',
+			],
+	     body    => <<EOF) or fail('Unable to submit test bug');
+Package: foo
+Severity: normal
+
+Rubber baby buggy bumpers
+EOF
+
+my $marker_pattern = qr/'\Q$from_email\E'/;
+my $safe_pattern = qr/ '--'.* '\Q$from_email\E'/;
+
+my $marker_found = 0;
+my $unsafe_found = 0;
+my $old_marker_found;
+my $old_unsafe_found;
+my $found_line;
+my $unsafe_line;
+
+$old_marker_found = $marker_found;
+$old_unsafe_found = $unsafe_found;
+$found_line = '';
+$unsafe_line = '';
+
+opendir(my $maildir, $sendmail_dir) or die "Unable to open $sendmail_dir: $!";
+for my $file (grep { !/^\.\.?$/ } readdir($maildir)) {
+  my $fh = IO::File->new("$sendmail_dir/$file", 'r') or next;
+  my $line = <$fh>;
+  next unless defined $line and $line =~ $marker_pattern;
+  $marker_found++;
+  $found_line = $line;
+  if ( $line !~ $safe_pattern ) {
+    $unsafe_found++;
+    $unsafe_line = $line;
+  }
+}
+closedir($maildir);
+
+ok($unsafe_found == $old_unsafe_found, $test_name)
+  or diag("User supplied From address could be treated as a flag to sendmail: $unsafe_line");
+
+if ($ENV{DEBUG}//0 and $found_line and $unsafe_found == $old_unsafe_found) {
+  diag("Sendmail invocation protected using --: $found_line");
+}
+
+
+
+# Once a bug report exists, control commands trigger scripts/process
+# to call Mail.pm:send_mail_message() with receipients set. In turn,
+# send_mail_message uses receipients in two places: an initial batched
+# attempt, and (if that fails) a per-recipient retry loop.
+
+
+# Test batched send_mail_message.
+$test_name = 'Batched reply to a control message';
+send_message(to      => 'control@bugs.something',
+	     headers => [To       => 'control@bugs.something',
+			 From     => $from_email,
+			 Subject  => 'Munging a bug for injection test',
+			],
+	     body    => <<EOF,
+debug 10
+tags 1 + security
+thanks
+EOF
+	    );
+
+$old_marker_found = $marker_found;
+$old_unsafe_found = $unsafe_found;
+$found_line = '';
+
+opendir($maildir, $sendmail_dir) or die "Unable to open $sendmail_dir: $!";
+for my $file (grep { !/^\.\.?$/ } readdir($maildir)) {
+  my $fh = IO::File->new("$sendmail_dir/$file", 'r') or next;
+  my $line = <$fh>;
+  next unless defined $line and $line =~ $marker_pattern;
+  $marker_found++;
+  $found_line = $line;
+  if ( $line !~ $safe_pattern ) {
+    $unsafe_found++;
+    $unsafe_line = $line;
+  }
+}
+closedir($maildir);
+
+ok($unsafe_found == $old_unsafe_found, $test_name)
+  or diag("User supplied From address could be treated as a flag to sendmail: $unsafe_line");
+
+if ($ENV{DEBUG}//0 and $found_line and $unsafe_found == $old_unsafe_found) {
+  diag("Sendmail invocation protected using --: $found_line");
+}
+
+
+
+# Exercise the retry loop in Mail.pm:send_mail_message().
+$test_name = 'Retry individual addresses replying to a control message';
+$ENV{DEBUG_MAIL_RETRY_LOOP} = 1;
+
+send_message(to      => 'control@bugs.something',
+	     headers => [To       => 'control@bugs.something',
+			 From     => $from_email,
+			 Subject  => 'Munging a bug for injection test',
+			],
+	     body    => <<EOF,
+debug 10
+tags 1 + wontfix
+thanks
+EOF
+	    );
+
+$old_marker_found = $marker_found;
+$old_unsafe_found = $unsafe_found;
+$found_line = '';
+
+opendir($maildir, $sendmail_dir) or die "Unable to open $sendmail_dir: $!";
+for my $file (grep { !/^\.\.?$/ } readdir($maildir)) {
+  my $fh = IO::File->new("$sendmail_dir/$file", 'r') or next;
+  my $line = <$fh>;
+  next unless defined $line and $line =~ $marker_pattern;
+  $marker_found++;
+  $found_line = $line;
+  if ( $line !~ $safe_pattern ) {
+    $unsafe_found++;
+    $unsafe_line = $line;
+  }
+}
+closedir($maildir);
+
+ok($unsafe_found == $old_unsafe_found, $test_name)
+  or diag("User supplied From address could be treated as a flag to sendmail: $unsafe_line");
+
+if ($ENV{DEBUG}//0 and $found_line and $unsafe_found == $old_unsafe_found) {
+  diag("Sendmail invocation protected using --: $found_line");
+}
-- 
2.53.0

