CGI and Perl

Changing Passwords from the Web

One of the most common requests I've seen for a CGI program is the one for a program that allows the Web client to change his/her password. Certainly, it's reasonable to want to be able to do this; unfortunately, it's more complicated and risky than it seems.

Because the httpd is running as the user nobody, and because the passwd file doesn't (or shouldn't) belong to that user, it's a bit of a quandary to have an exec'd CGI program change that file. There are alternatives, however. You can run a secondary server on a different port, under the www userid, whose only purpose would be to execute the CGI program that changes the user's password, for instance. Another alternative might be to implement something with SafeCGIPerl, discussed later, and the HTTPD::UserAdmin module just introduced. Coding it up is left as an exercise for the wary administrator. Be very, very careful.

Monitoring Userids

As a final note in this section, we'll add a bit more code to our ongoing example to keep track of userids in the .htpasswd file for each <Directory> entry in access.conf.

use HTTPD::Config;
 require HTTPD::UserAdmin;
 require "stat.pl";
 $conf =  `/usr/local/etc/httpd/conf';
 @files = qw(httpd.conf srm.conf access.conf);
 $V= new HTTPD::Config (SERVER => Apache,
                              SERVER_ROOT => $conf,
                              FILES => [@files]);
 print "Userid: ", $V->user,"\n";
 print "Group: ", $V->group,"\n";
 print "Administrator is: ", $V->server_admin,"\n";
 print "Running at port: ", $V->port,"\n";
 print "Access filename: ",$V->access_file_name,"\n";
 print "User Directory: ", $V->user_dir,"\n";
 print "Global Types:\t", join("\n\t\t",$V->add_type),"\n";
 print "\n\n";
 foreach $dir (keys %{$V->{`Directory'}}){
     print "Options for Directory: $dir\n";
     while(($opt,$val) = each %{$V->{`Directory'}{$dir}{`OPTIONS'}}){
         print "\t",$opt, " : ", @{$val},"\n";
         if($opt eq "AuthUserFile"){
             ($path = join(`',@{$val})) =~ s/^(.*)\/.*/$1/;
             print "\tCurrent users in $opt:\n";
             $users = new HTTPD::UserAdmin(DBType => "Text",
                 Path => $path, Locking => 0, Server => "apache");
             #@users = $users->list;
             foreach $user (sort($users->list)){
                 print "\t\t$user : ";
                 print $users->password($user),"\n";
             }
         }
     }
     print "\tLimit: @{$V->{Directory}{$dir}{LIMIT}{METHODS}}\n";
     while(($key,$val) = each %{$V->{Directory}{$dir}{LIMIT}{OPTIONS}}) {
         print "\t\t$key = @{$val}\n";
     }
     print "\n";
 }
 # rudimentary permissions checking
 $webuser = (getpwnam($V->user))[2];
 opendir(ROOT,$V->server_root);
 @files = grep(!/\.\.?/,readdir(ROOT));
 closedir(ROOT);
 foreach $f (@files){
     @s = Stat($V->server_root."/$f");
     if($s[$ST_UID] == $webuser){
         print "Warning: ",$V->server_root,"/$f is owned by ",
                 $V->user,"\n\n";
     }
     if($f eq "httpd"){
         if(($s[$ST_MODE] != 0100700) or ($s[$ST_UID] != 0)){
             print "\tWarning: ",$V->server_root,"/httpd may have\n";
             print "\tpermission problems.  Recommend root ownership\n";
             print "\tand readable, writable and executable only by\n";
             print "\troot user\n";
         }
     }
 }

We get a fairly nice report back that looks like this:

Userid: nobody
 Group: nogroup
 Administrator is: [email protected]
 Running at port: 80
 Access filename: .privaccess
 User Directory: public_html
 Global Types:   text/html .shtml
 Options for Directory: /usr/local/etc/httpd/cgi-bin
         Options : None
         AllowOverride : None
         Limit:
 Options for Directory: /usr/local/etc/httpd/htdocs/test1
         Options : None
         AllowOverride : None
         Limit: GET POST
                 require = valid-user
 Options for Directory: /usr/local/etc/httpd/htdocs
         PerlHandler : main::handler
         AuthUserFile : /usr/local/etc/httpd/conf/.htpasswd
         Current users in AuthUserFile:
                 bmiddlet : ztTO6y2K.3qLE
                 bozo : 0Euk1WMWyXRgg
                 josie : _LhTDVY/y6tPo
         AddType : text/html .shtml
         AuthName : PasswordAdmin
         Options : Indexes SymLinksIfOwnerMatch IncludesNOEXEC
         AuthType : Basic
         AllowOverride : AuthConfig FileInfo Indexes Limit
         Limit: GET
                 deny = from .bozo.com
                 order = mutual-failure
                 allow = from .metronet.com

Of course, if your user database gets very large, then this script may produce more output than you want to see. The idea from the beginning has been that this script would run automatically via cron, having its output compared to the expected output, possibly from the day before, then sending on only the differences for your inspection.