Index: MANIFEST =================================================================== --- MANIFEST (revision 545493) +++ MANIFEST (working copy) @@ -468,6 +468,13 @@ t/whitelist_subject.t t/whitelist_to.t t/zz_cleanup.t +t/root_spamd.t +t/root_spamd_tell.t +t/root_spamd_tell_paranoid.t +t/root_spamd_tell_x.t +t/root_spamd_tell_x_paranoid.t +t/root_spamd_x.t +t/root_spamd_x_paranoid.t tools/README.speedtest tools/check_whitelist tools/convert_awl_dbm_to_sql Index: lib/Mail/SpamAssassin.pm =================================================================== --- lib/Mail/SpamAssassin.pm (revision 545493) +++ lib/Mail/SpamAssassin.pm (working copy) @@ -96,7 +96,7 @@ }; $VERSION = "3.001009"; # update after release (same format as perl $]) -$IS_DEVEL_BUILD = 1; # change for release versions +# $IS_DEVEL_BUILD = 1; # change for release versions @ISA = qw(); Index: t/root_spamd_tell_x.t =================================================================== --- t/root_spamd_tell_x.t (revision 0) +++ t/root_spamd_tell_x.t (revision 0) @@ -0,0 +1,58 @@ +#!/usr/bin/perl + +use lib '.'; use lib 't'; +use SATest; sa_t_init("root_spamd_tell_x"); +use Test; + +use constant TEST_ENABLED => conf_bool('run_root_tests'); +use constant IS_ROOT => eval { ($> == 0); }; +use constant RUN_TESTS => (TEST_ENABLED && IS_ROOT); + +BEGIN { plan tests => (RUN_TESTS ? 6 : 0) }; +exit unless RUN_TESTS; + +# --------------------------------------------------------------------------- + +%patterns = ( +q{ Message successfully } => 'learned', +); + +# run spamc as unpriv uid +$spamc = "sudo -u nobody $spamc"; + +# remove these first +unlink('log/user_state/bayes_seen'); +unlink('log/user_state/bayes_toks'); + +# ensure it is writable by all +use File::Path; mkpath("log/user_state"); chmod 01777, "log/user_state"; + +ok(start_spamd("-L --allow-tell --create-prefs -x")); + +ok(spamcrun("-lx -L ham < data/spam/001", \&patterns_run_cb)); +ok_all_patterns(); + +ok(stop_spamd()); + +# ensure these are not owned by root +ok check_owner('log/user_state/bayes_seen'); +ok check_owner('log/user_state/bayes_toks'); + +sub check_owner { + my $f = shift; + my @stat = stat $f; + + print "stat($f) = ".join(', ', @stat)."\n"; + + if (!defined $stat[1]) { + warn "no stat for $f"; + return 0; + } + elsif ($stat[4] == 0) { + warn "stat for $f: owner is root"; + return 0; + } + else { + return 1; + } +} Index: t/root_spamd_tell.t =================================================================== --- t/root_spamd_tell.t (revision 0) +++ t/root_spamd_tell.t (revision 0) @@ -0,0 +1,58 @@ +#!/usr/bin/perl + +use lib '.'; use lib 't'; +use SATest; sa_t_init("root_spamd_tell"); +use Test; + +use constant TEST_ENABLED => conf_bool('run_root_tests'); +use constant IS_ROOT => eval { ($> == 0); }; +use constant RUN_TESTS => (TEST_ENABLED && IS_ROOT); + +BEGIN { plan tests => (RUN_TESTS ? 6 : 0) }; +exit unless RUN_TESTS; + +# --------------------------------------------------------------------------- + +%patterns = ( +q{ Message successfully } => 'learned', +); + +# run spamc as unpriv uid +$spamc = "sudo -u nobody $spamc"; + +# remove these first +unlink('log/user_state/bayes_seen'); +unlink('log/user_state/bayes_toks'); + +# ensure it is writable by all +use File::Path; mkpath("log/user_state"); chmod 01777, "log/user_state"; + +ok(start_spamd("-L --allow-tell")); + +ok(spamcrun("-lx -L ham < data/spam/001", \&patterns_run_cb)); +ok_all_patterns(); + +ok(stop_spamd()); + +# ensure these are not owned by root +ok check_owner('log/user_state/bayes_seen'); +ok check_owner('log/user_state/bayes_toks'); + +sub check_owner { + my $f = shift; + my @stat = stat $f; + + print "stat($f) = ".join(', ', @stat)."\n"; + + if (!defined $stat[1]) { + warn "no stat for $f"; + return 0; + } + elsif ($stat[4] == 0) { + warn "stat for $f: owner is root"; + return 0; + } + else { + return 1; + } +} Index: t/root_spamd_tell_paranoid.t =================================================================== --- t/root_spamd_tell_paranoid.t (revision 0) +++ t/root_spamd_tell_paranoid.t (revision 0) @@ -0,0 +1,58 @@ +#!/usr/bin/perl + +use lib '.'; use lib 't'; +use SATest; sa_t_init("root_spamd_tell_paranoid"); +use Test; + +use constant TEST_ENABLED => conf_bool('run_root_tests'); +use constant IS_ROOT => eval { ($> == 0); }; +use constant RUN_TESTS => (TEST_ENABLED && IS_ROOT); + +BEGIN { plan tests => (RUN_TESTS ? 6 : 0) }; +exit unless RUN_TESTS; + +# --------------------------------------------------------------------------- + +%patterns = ( +q{ Message successfully } => 'learned', +); + +# run spamc as unpriv uid +$spamc = "sudo -u nobody $spamc"; + +# remove these first +unlink('log/user_state/bayes_seen'); +unlink('log/user_state/bayes_toks'); + +# ensure it is writable by all +use File::Path; mkpath("log/user_state"); chmod 01777, "log/user_state"; + +ok(start_spamd("-L --allow-tell --paranoid")); + +ok(spamcrun("-lx -L ham < data/spam/001", \&patterns_run_cb)); +ok_all_patterns(); + +ok(stop_spamd()); + +# ensure these are not owned by root +ok check_owner('log/user_state/bayes_seen'); +ok check_owner('log/user_state/bayes_toks'); + +sub check_owner { + my $f = shift; + my @stat = stat $f; + + print "stat($f) = ".join(', ', @stat)."\n"; + + if (!defined $stat[1]) { + warn "no stat for $f"; + return 0; + } + elsif ($stat[4] == 0) { + warn "stat for $f: owner is root"; + return 0; + } + else { + return 1; + } +} Index: t/root_spamd_x_paranoid.t =================================================================== --- t/root_spamd_x_paranoid.t (revision 0) +++ t/root_spamd_x_paranoid.t (revision 0) @@ -0,0 +1,46 @@ +#!/usr/bin/perl + +use lib '.'; use lib 't'; +use SATest; sa_t_init("root_spamd_x_paranoid"); +use Test; + +use constant TEST_ENABLED => conf_bool('run_root_tests'); +use constant IS_ROOT => eval { ($> == 0); }; +use constant RUN_TESTS => (TEST_ENABLED && IS_ROOT); + +BEGIN { plan tests => (RUN_TESTS ? 14 : 0) }; +exit unless RUN_TESTS; + +# --------------------------------------------------------------------------- + +%patterns = ( + +q{ Return-Path: sb55sb55@yahoo.com}, 'firstline', +q{ Subject: There yours for FREE!}, 'subj', +q{ X-Spam-Status: Yes, score=}, 'status', +q{ X-Spam-Flag: YES}, 'flag', +q{ X-Spam-Level: **********}, 'stars', +q{ TEST_ENDSNUMS}, 'endsinnums', +q{ TEST_NOREALNAME}, 'noreal', +q{ This must be the very last line}, 'lastline', + +); + +# run spamc as unpriv uid +$spamc = "sudo -u nobody $spamc"; + +ok(start_spamd("-L --create-prefs -x --paranoid")); + +ok(spamcrun("< data/spam/001", \&patterns_run_cb)); +ok_all_patterns(); + +%patterns = ( +q{ X-Spam-Status: Yes, score=}, 'status', +q{ X-Spam-Flag: YES}, 'flag', + ); + + +ok (spamcrun("< data/spam/018", \&patterns_run_cb)); +ok_all_patterns(); + +ok(stop_spamd()); Index: t/root_spamd_x.t =================================================================== --- t/root_spamd_x.t (revision 0) +++ t/root_spamd_x.t (revision 0) @@ -0,0 +1,46 @@ +#!/usr/bin/perl + +use lib '.'; use lib 't'; +use SATest; sa_t_init("root_spamd_x"); +use Test; + +use constant TEST_ENABLED => conf_bool('run_root_tests'); +use constant IS_ROOT => eval { ($> == 0); }; +use constant RUN_TESTS => (TEST_ENABLED && IS_ROOT); + +BEGIN { plan tests => (RUN_TESTS ? 14 : 0) }; +exit unless RUN_TESTS; + +# --------------------------------------------------------------------------- + +%patterns = ( + +q{ Return-Path: sb55sb55@yahoo.com}, 'firstline', +q{ Subject: There yours for FREE!}, 'subj', +q{ X-Spam-Status: Yes, score=}, 'status', +q{ X-Spam-Flag: YES}, 'flag', +q{ X-Spam-Level: **********}, 'stars', +q{ TEST_ENDSNUMS}, 'endsinnums', +q{ TEST_NOREALNAME}, 'noreal', +q{ This must be the very last line}, 'lastline', + +); + +# run spamc as unpriv uid +$spamc = "sudo -u nobody $spamc"; + +ok(start_spamd("-L --create-prefs -x")); + +ok(spamcrun("< data/spam/001", \&patterns_run_cb)); +ok_all_patterns(); + +%patterns = ( +q{ X-Spam-Status: Yes, score=}, 'status', +q{ X-Spam-Flag: YES}, 'flag', + ); + + +ok (spamcrun("< data/spam/018", \&patterns_run_cb)); +ok_all_patterns(); + +ok(stop_spamd()); Index: t/root_spamd.t =================================================================== --- t/root_spamd.t (revision 0) +++ t/root_spamd.t (revision 0) @@ -0,0 +1,48 @@ +#!/usr/bin/perl + +# run with: sudo prove -v t/root_spamd* + +use lib '.'; use lib 't'; +use SATest; sa_t_init("root_spamd"); +use Test; + +use constant TEST_ENABLED => conf_bool('run_root_tests'); +use constant IS_ROOT => eval { ($> == 0); }; +use constant RUN_TESTS => (TEST_ENABLED && IS_ROOT); + +BEGIN { plan tests => (RUN_TESTS ? 14 : 0) }; +exit unless RUN_TESTS; + +# --------------------------------------------------------------------------- + +%patterns = ( + +q{ Return-Path: sb55sb55@yahoo.com}, 'firstline', +q{ Subject: There yours for FREE!}, 'subj', +q{ X-Spam-Status: Yes, score=}, 'status', +q{ X-Spam-Flag: YES}, 'flag', +q{ X-Spam-Level: **********}, 'stars', +q{ TEST_ENDSNUMS}, 'endsinnums', +q{ TEST_NOREALNAME}, 'noreal', +q{ This must be the very last line}, 'lastline', + +); + +# run spamc as unpriv uid +$spamc = "sudo -u nobody $spamc"; + +ok(start_spamd("-L")); + +ok(spamcrun("< data/spam/001", \&patterns_run_cb)); +ok_all_patterns(); + +%patterns = ( +q{ X-Spam-Status: Yes, score=}, 'status', +q{ X-Spam-Flag: YES}, 'flag', + ); + + +ok (spamcrun("< data/spam/018", \&patterns_run_cb)); +ok_all_patterns(); + +ok(stop_spamd()); Index: t/SATest.pm =================================================================== --- t/SATest.pm (revision 545493) +++ t/SATest.pm (working copy) @@ -138,6 +138,8 @@ $ENV{'TEST_DIR'} = $cwd; $testname = $tname; + + $current_user = (getpwuid($>))[0]; } # a port number between 32768 and 65535; used to allow multiple test Index: t/root_spamd_tell_x_paranoid.t =================================================================== --- t/root_spamd_tell_x_paranoid.t (revision 0) +++ t/root_spamd_tell_x_paranoid.t (revision 0) @@ -0,0 +1,58 @@ +#!/usr/bin/perl + +use lib '.'; use lib 't'; +use SATest; sa_t_init("root_spamd_tell_x_paranoid"); +use Test; + +use constant TEST_ENABLED => conf_bool('run_root_tests'); +use constant IS_ROOT => eval { ($> == 0); }; +use constant RUN_TESTS => (TEST_ENABLED && IS_ROOT); + +BEGIN { plan tests => (RUN_TESTS ? 6 : 0) }; +exit unless RUN_TESTS; + +# --------------------------------------------------------------------------- + +%patterns = ( +q{ Message successfully } => 'learned', +); + +# run spamc as unpriv uid +$spamc = "sudo -u nobody $spamc"; + +# remove these first +unlink('log/user_state/bayes_seen'); +unlink('log/user_state/bayes_toks'); + +# ensure it is writable by all +use File::Path; mkpath("log/user_state"); chmod 01777, "log/user_state"; + +ok(start_spamd("-L --allow-tell --create-prefs -x --paranoid")); + +ok(spamcrun("-lx -L ham < data/spam/001", \&patterns_run_cb)); +ok_all_patterns(); + +ok(stop_spamd()); + +# ensure these are not owned by root +ok check_owner('log/user_state/bayes_seen'); +ok check_owner('log/user_state/bayes_toks'); + +sub check_owner { + my $f = shift; + my @stat = stat $f; + + print "stat($f) = ".join(', ', @stat)."\n"; + + if (!defined $stat[1]) { + warn "no stat for $f"; + return 0; + } + elsif ($stat[4] == 0) { + warn "stat for $f: owner is root"; + return 0; + } + else { + return 1; + } +} Index: t/config.dist =================================================================== --- t/config.dist (revision 545493) +++ t/config.dist (working copy) @@ -45,3 +45,9 @@ # this to "y". run_spamd_prefork_stress_test=n +# --------------------------------------------------------------------------- + +# The "root_*.t" tests require root privileges, and may create files in +# the filesystem as part of the test. Disabled by default. +run_root_tests=n + Index: t/spamd_allow_user_rules.t =================================================================== --- t/spamd_allow_user_rules.t (revision 545493) +++ t/spamd_allow_user_rules.t (working copy) @@ -36,7 +36,7 @@ "; close OUT; -ok (start_spamd ("--virtual-config-dir=log/virtualconfig/%u -L")); +ok (start_spamd ("--virtual-config-dir=log/virtualconfig/%u -L -u $current_user")); ok (spamcrun ("-u testuser < data/spam/009", \&patterns_run_cb)); ok (stop_spamd ()); Index: spamd/spamd.raw =================================================================== --- spamd/spamd.raw (revision 545493) +++ spamd/spamd.raw (working copy) @@ -413,15 +413,29 @@ # support setuid() to user unless: # run with -u # we're not root -# doing --vpopmail +# doing --vpopmail or --virtual-config-dir # we disable user-config my $setuid_to_user = ( $opt{'username'} || $> != 0 || $opt{'vpopmail'} || - (!$opt{'user-config'} && !($opt{'setuid-with-sql'}||$opt{'setuid-with-ldap'})) - ) ? 0 : 1; + $opt{'virtual-config-dir'} + ) ? 0 : 1; +dbg("spamd: will perform setuids? $setuid_to_user"); + +if ( $opt{'vpopmail'} ) { + if ( !$opt{'username'} ) { + die "spamd: cannot use --vpopmail without -u\n"; + } +} + +if ( $opt{'virtual-config-dir'} ) { + if ( !$opt{'username'} ) { + die "spamd: cannot use --virtual-config-dir without -u\n"; + } +} + # always copy the config, later code may disable my $copy_config_p = 1; @@ -1213,19 +1227,9 @@ $expected_length = $hdrs->{expected_length}; } - handle_setuid_to_user if ($setuid_to_user && $> == 0); + return 0 unless do_user_handling(); + if ($> == 0) { die "spamd: still running as root! dying"; } - if ( $opt{'sql-config'} && !defined($current_user) ) { - unless ( handle_user_sql('nobody') ) { - service_unavailable_error("Error fetching user preferences via SQL"); - return 0; - } - } - - if ( $opt{'ldap-config'} && !defined($current_user) ) { - handle_user_ldap('nobody'); - } - my $resp = "EX_OK"; # generate mail object from input @@ -1389,6 +1393,9 @@ $expected_length = $hdrs->{expected_length}; + return 0 unless do_user_handling(); + if ($> == 0) { die "spamd: still running as root! dying"; } + if (!$opt{tell}) { service_unavailable_error("TELL commands have not been enabled."); return 0; @@ -1404,8 +1411,6 @@ return 0; } - &handle_setuid_to_user if ($setuid_to_user && $> == 0); - if ($opt{'sql-config'} && !defined($current_user)) { unless (handle_user_sql('nobody')) { service_unavailable_error("Error fetching user preferences via SQL"); @@ -1503,6 +1508,26 @@ ########################################################################### +sub do_user_handling { + if ($setuid_to_user && $> == 0) { + handle_setuid_to_user(); + } + + if ( $opt{'sql-config'} && !defined($current_user) ) { + unless ( handle_user_sql('nobody') ) { + service_unavailable_error("Error fetching user preferences via SQL"); + return 0; + } + } + + if ( $opt{'ldap-config'} && !defined($current_user) ) { + handle_user_ldap('nobody'); + } + + dbg ("spamd: running as uid $>"); + return 1; +} + # generalised header parser. sub parse_headers { my ($hdrs, $client) = @_; @@ -1592,9 +1617,12 @@ handle_user_setuid_with_ldap($current_user); $setuid_to_user = 1; # as above } + else { + handle_user_setuid_basic($current_user); + } } else { - handle_user($current_user); + handle_user_setuid_basic($current_user); if ( $opt{'sql-config'} ) { unless ( handle_user_sql($current_user) ) { service_unavailable_error("Error fetching user preferences via SQL"); @@ -1698,7 +1726,7 @@ return 1; } -sub handle_user { +sub handle_user_setuid_basic { my $username = shift; # @@ -1742,6 +1770,14 @@ } } + if ($opt{'user-config'}) { + handle_user_set_user_prefs($dir, $username); + } +} + +sub handle_user_set_user_prefs { + my ($dir, $username) = @_; + # # If vpopmail config enabled then set $dir to virtual homedir # @@ -1767,9 +1803,6 @@ # If vpopmail config enabled then pass virtual homedir onto create_default_cf_needed # if ( $opt{'vpopmail'} ) { - if ( !$opt{'username'} ) { - warn "spamd: cannot use vpopmail without -u\n"; - } create_default_cf_if_needed( $cf_file, $username, $dir ); $spamtest->read_scoreonly_config($cf_file); $spamtest->signal_user_changed( @@ -2382,6 +2415,8 @@ The pattern B expand to an absolute directory when spamd is running daemonized (B<-d>). +Currently, use of this without B<-u> is not supported. This inhibits setuid. + =item B<-r> I, B<--pidfile>=I Write the process ID of the spamd parent to the file specified by I. @@ -2395,7 +2430,7 @@ maildir. This option is useful for vpopmail virtual users who do not have an entry in the system /etc/passwd file. -Currently, use of this without B<-u> is not supported. +Currently, use of this without B<-u> is not supported. This inhibits setuid. =item B<-s> I, B<--syslog>=I