--- spamd.raw.orig Mon Oct 6 22:29:46 2003 +++ spamd.raw Mon Oct 6 23:35:39 2003 @@ -449,10 +449,6 @@ logmsg("server started on $listeninfo (r my $current_user; my $client; -# qmail vpopmail support -my $assign = "/var/qmail/users/assign"; # where to find domain to user mappings -my (%qmailu, $qu_load); - my $readvec = ""; my %pipes = (); vec($readvec, $server->fileno, 1) = 1; @@ -874,24 +870,20 @@ sub auth_ident sub handle_user { - my $username = shift; + my($username) = @_; - # + my $userid = $username; # If vpopmail config enabled then look up userinfo for vpopmail uid - # as defined by $opt{'username'} or as passed via $username - # - my $userid = ''; - if ($opt{'vpopmail'} && $opt{'username'}) { - $userid = $opt{'username'}; - } elsif ( $opt{'vpopmail'} ) { - $userid = "vpopmail"; - } else { - $userid = $username; + # as defined by $opt{'username'}. + if ($opt{'vpopmail'}) { + $userid = $opt{'username'} || 'vpopmail'; } - my ($name,$pwd,$uid,$gid,$quota,$comment,$gcos,$dir,$etc) = - getpwnam($userid); - if ( !$spamtest->{'paranoid'} && !defined($uid) ) { + # Get UID, GID and home directory; those aren't tainted according to + # perlsec. + my($uid, $gid, $userhome) = (getpwnam($userid))[2, 3, 7]; + + if (!defined($uid) && !$spamtest->{'paranoid'}) { #if we are given a username, but can't look it up, #Maybe NIS is down? lets break out here to allow #them to get 'defaults' when we are not running paranoid. @@ -899,53 +891,70 @@ sub handle_user return 0; } - # not sure if this is required, the doco says it isn't - $uid =~ /^(\d+)$/ and $uid = $1; # de-taint - $gid =~ /^(\d+)$/ and $gid = $1; # de-taint - if ($setuid_to_user) { $) = "$gid $gid"; # change eGID $> = $uid; # change eUID if ( !defined($uid) || ($> != $uid and $> != ($uid-2**32))) { logmsg "fatal: setuid to $username failed"; die; # make it fatal to avoid security breaches - } else { - logmsg "info: setuid to $username succeeded"; } + logmsg "info: setuid to $username succeeded"; } - # - # If vpopmail config enabled then set $dir to virtual homedir - # + # If vpopmail config enabled then set $homedir to virtual homedir if ($opt{'vpopmail'}) { - $dir = `$dir/bin/vuserinfo -d $username`; - chomp ($dir); + # TODO: This path should be configurable, too. + my $vuserinfo = File::Spec->catfile( + $userhome, + "bin", + "vuserinfo" + ); + if (-x $vuserinfo) { + $userhome = Mail::SpamAssassin::Util::untaint_file_path( + qx($vuserinfo -d $username) + ); + } + if ($! || !-x _) { + logmsg "vpopmail: could not execute $vuserinfo: " . (-x _ ? $! : "No such executable"); + return 0; + } } - my $cf_file = $dir."/.spamassassin/user_prefs"; - # - # If vpopmail config enabled then pass virtual homedir onto create_default_cf_needed - # - if ($opt{'vpopmail'}) { - if ($opt{'username'}) { - create_default_cf_if_needed ($cf_file, $username, $dir); - $spamtest->read_scoreonly_config ($cf_file); - $spamtest->signal_user_changed ({ username => $username, - user_dir => "$dir" }); - } else { - my $sysnam = get_user_from_address ($username); - $spamtest->read_scoreonly_config ($cf_file); - $spamtest->signal_user_changed ({ username => $sysnam, - user_dir => "$dir" }) + my $cf_file = File::Spec->catfile( + $userhome, + ".spamassassin", + "user_prefs" + ); + if (!$opt{'vpopmail'}) { + create_default_cf_if_needed( + $cf_file, + $username, + ); + } + else { + $username = map_address_to_vpopmail_user($username); + if (!$username) { + logmsg "vpopmail: no user for that address"; + return 0; } - } else { - create_default_cf_if_needed ($cf_file, $username); - $spamtest->read_scoreonly_config ($cf_file); - $spamtest->signal_user_changed ({ username => $username, - user_dir => $dir }); + # If vpopmail config enabled then pass virtual homedir onto + # create_default_cf_if_needed + if ($opt{'username'}) { + create_default_cf_if_needed( + $cf_file, + $username, + $userhome, # this parameter is only set for vpopmail... + ); + } } + $spamtest->read_scoreonly_config($cf_file); + $spamtest->signal_user_changed({ + username => $username, + user_dir => $userhome, + }); + return 1; } @@ -1083,47 +1092,107 @@ sub create_default_cf_if_needed { } } -sub get_user_from_address { - my ($user, $domain) = split(/@/, $_[0]); - my $dom = lc($domain); - - if ( $qmailu{$dom} ne "" ) { - warn "returning result from cache.\n" if ($opt{'debug'}); - my $nam = getpwuid($qmailu{$dom}); - return $nam; - } else { - warn "cache miss\n" if ($opt{'debug'}); - &fill_qmailu_cache($assign); - if ( $qmailu{$dom} ne "" ) { - my $nam = getpwuid($qmailu{$dom}); - return $nam; + +sub map_address_to_vpopmail_user { + my($user, $domain) = split(/@/, $_[0]); + + if ($domain) { + my $uid = query_qmail_assign_cache($domain); + if (!defined ($uid)) { + warn "vpopmail: no uid for domain $domain" if $opt{'debug'}; + return undef; } else { - return 0; + warn "vpopmail: domain $domain maps to uid $uid" if $opt{'debug'}; } + $user = getpwuid($uid); } + + return $user || undef; } -sub fill_qmailu_cache { - # rather than parsing the qmail users/assign file every time a message +{ + # Where to find domain to user mappings + # TODO: This should be made configurable some day + my $qmail_assign_file = "var/qmail/users/assign"; + # The cache hash and its timestamp. + my %qmail_assign_cache = (); + my $qmail_assign_cache = 0; + + sub query_qmail_assign_cache { + my($user) = @_; + + # Rather than parsing the qmail users/assign file every time a message # arrives, we run this once when spamd is loaded and check the files # modified time each query to know when to reload it. + my($timestamp) = (stat($_[0]))[9]; + if ($timestamp > $qmail_assign_cache) { + warn "qmail: (re-)caching qmail-users assign file $qmail_assign_file..." if $opt{'debug'}; + if (fill_qmail_assign_cache()) { + warn "qmail: done." if $opt{'debug'}; + } else { + warn "qmail: failed." if $opt{'debug'}; + } + } + else { + warn "qmail: qmail-users assign cache up-to-date." if $opt{'debug'}; + } - my ($READ, $WRITE) = (stat($_[0]))[8,9]; - if ( $WRITE > $qu_load ) { - undef %qmailu; - $qu_load = time; - open(ASSIGN, $_[0]) || die "couldn't open $_[0]: $!\n"; - warn "loading $_[0] into cache...." if ($opt{'debug'}); - while() { - my @data = split(/:/, $_); - $qmailu{$data[1]} = $data[2]; - }; - warn "done.\n" if ($opt{'debug'}); - close(ASSIGN); + $user = lc($user); + return $qmail_assign_cache{$user} || undef; + } + + sub fill_qmail_assign_cache { + %qmail_assign_cache = (); + + # From qmail-users(5): + # | The file /var/qmail/users/assign assigns addresses to users. For exam- + # | ple, + # | + # | =joe.shmoe:joe:503:78:/home/joe::: + # | + # | says that mail for joe.shmoe should be delivered to user joe, with uid + # | 503 and gid 78, as specified by /home/joe/.qmail. + unless (open(ASSIGN, $qmail_assign_file)) { + logmsg "error: qmail: couldn't open assign file $qmail_assign_file: $!\n"; + return 0; + } + my $l = 0; + while () { + $l++; + next if /^\s*#/; + my @data = split /:/; + + # Untaint the values... + if (defined($data[1]) && $data[1] =~ /^([a-z0-9.-]+)$/i) { + $data[1] = lc($1); + } else { + logmsg sprintf("warning: qmail: skipping line %i in assign file: invalid value for %s: %s", + $l, + "user", + $data[1] || '(none)', + ); + next; + } + if (defined($data[2]) && $data[2] =~ /^([0-9]+)$/) { + $data[2] = $1; } else { - warn "$_[0] already cached.\n" if ($opt{'debug'}); + logmsg sprintf("warning: qmail: skipping line %i in assign file: invalid value for %s: %s", + $l, + "uid", + $data[2] || '(none)', + ); + next; + } + + # ... and store them. + $qmail_assign_cache{$data[1]} = $data[2]; + } + close(ASSIGN); } + + return 1; } + sub logmsg {