diff options
-rw-r--r-- | Bugzilla/Token.pm | 184 | ||||
-rw-r--r-- | CGI.pl | 122 | ||||
-rw-r--r-- | Token.pm | 184 | ||||
-rwxr-xr-x | checksetup.pl | 107 | ||||
-rwxr-xr-x | createaccount.cgi | 17 | ||||
-rwxr-xr-x | editusers.cgi | 64 | ||||
-rw-r--r-- | globals.pl | 109 | ||||
-rwxr-xr-x | sanitycheck.cgi | 19 | ||||
-rwxr-xr-x | token.cgi | 243 | ||||
-rwxr-xr-x | userprefs.cgi | 19 |
10 files changed, 938 insertions, 130 deletions
diff --git a/Bugzilla/Token.pm b/Bugzilla/Token.pm new file mode 100644 index 000000000..cde97f87e --- /dev/null +++ b/Bugzilla/Token.pm @@ -0,0 +1,184 @@ +#!/usr/bonsaitools/bin/perl -w +# -*- Mode: perl; indent-tabs-mode: nil -*- +# +# The contents of this file are subject to the Mozilla Public +# License Version 1.1 (the "License"); you may not use this file +# except in compliance with the License. You may obtain a copy of +# the License at http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS +# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or +# implied. See the License for the specific language governing +# rights and limitations under the License. +# +# The Original Code is the Bugzilla Bug Tracking System. +# +# The Initial Developer of the Original Code is Netscape Communications +# Corporation. Portions created by Netscape are +# Copyright (C) 1998 Netscape Communications Corporation. All +# Rights Reserved. +# +# Contributor(s): Myk Melez <myk@mozilla.org> + +################################################################################ +# Module Initialization +################################################################################ + +# Make it harder for us to do dangerous things in Perl. +use diagnostics; +use strict; + +# Bundle the functions in this file together into the "Token" package. +package Token; + +# This module requires that its caller have said "require CGI.pl" to import +# relevant functions from that script and its companion globals.pl. + +################################################################################ +# Functions +################################################################################ + +sub IssuePasswordToken { + # Generates a random token, adds it to the tokens table, and sends it + # to the user with instructions for using it to change their password. + + my ($loginname) = @_; + + # Retrieve the user's ID from the database. + my $quotedloginname = &::SqlQuote($loginname); + &::SendSQL("SELECT userid FROM profiles WHERE login_name = $quotedloginname"); + my ($userid) = &::FetchSQLData(); + + # Generate a unique token and insert it into the tokens table. + # We have to lock the tokens table before generating the token, + # since the database must be queried for token uniqueness. + &::SendSQL("LOCK TABLE tokens WRITE"); + my $token = GenerateUniqueToken(); + my $quotedtoken = &::SqlQuote($token); + my $quotedipaddr = &::SqlQuote($::ENV{'REMOTE_ADDR'}); + &::SendSQL("INSERT INTO tokens ( userid , issuedate , token , tokentype , eventdata ) + VALUES ( $userid , NOW() , $quotedtoken , 'password' , $quotedipaddr )"); + &::SendSQL("UNLOCK TABLES"); + + # Mail the user the token along with instructions for using it. + MailPasswordToken($loginname, $token); + +} + + +sub GenerateUniqueToken { + # Generates a unique random token. Uses &GenerateRandomPassword + # for the tokens themselves and checks uniqueness by searching for + # the token in the "tokens" table. Gives up if it can't come up + # with a token after about one hundred tries. + + my $token; + my $duplicate = 1; + my $tries = 0; + while ($duplicate) { + + ++$tries; + if ($tries > 100) { + &::DisplayError("Something is seriously wrong with the token generation system."); + exit; + } + + $token = &::GenerateRandomPassword(); + &::SendSQL("SELECT userid FROM tokens WHERE token = " . &::SqlQuote($token)); + $duplicate = &::FetchSQLData(); + } + + return $token; + +} + +sub MailPasswordToken { + # Emails a password token to a user along with instructions for its use. + # Called exclusively from &IssuePasswordToken. + + my ($emailaddress, $token) = @_; + + my $urlbase = &::Param("urlbase"); + my $emailsuffix = &::Param('emailsuffix'); + + open SENDMAIL, "|/usr/lib/sendmail -t"; + + print SENDMAIL qq|From: bugzilla-daemon +To: $emailaddress$emailsuffix +Subject: Bugzilla Change Password Request + +You or someone impersonating you has requested to change your Bugzilla +password. To change your password, visit the following link: + +${urlbase}token.cgi?a=cfmpw&t=$token + +If you are not the person who made this request, or you wish to cancel +this request, visit the following link: + +${urlbase}token.cgi?a=cxlpw&t=$token +|; + close SENDMAIL; +} + +sub Cancel { + # Cancels a previously issued token and notifies the system administrator. + # This should only happen when the user accidentally makes a token request + # or when a malicious hacker makes a token request on behalf of a user. + + my ($token, $cancelaction) = @_; + + # Quote the token for inclusion in SQL statements. + my $quotedtoken = &::SqlQuote($token); + + # Get information about the token being cancelled. + &::SendSQL("SELECT issuedate , tokentype , eventdata , login_name , realname + FROM tokens, profiles + WHERE tokens.userid = profiles.userid + AND token = $quotedtoken"); + my ($issuedate, $tokentype, $eventdata, $loginname, $realname) = &::FetchSQLData(); + + # Get the email address of the Bugzilla maintainer. + my $maintainer = &::Param('maintainer'); + + # Format the user's real name and email address into a single string. + my $username = $realname ? $realname . " <" . $loginname . ">" : $loginname; + + # Notify the user via email about the cancellation. + open SENDMAIL, "|/usr/lib/sendmail -t"; + print SENDMAIL qq|From: bugzilla-daemon +To: $username +Subject: "$tokentype" token cancelled + +A token was cancelled from $::ENV{'REMOTE_ADDR'}. This is either +an honest mistake or the result of a malicious hack attempt. +Take a look at the information below and forward this email +to $maintainer if you suspect foul play. + + Token: $token + Token Type: $tokentype + User: $username + Issue Date: $issuedate + Event Data: $eventdata + +Cancelled Because: $cancelaction +|; + close SENDMAIL; + + # Delete the token from the database. + &::SendSQL("LOCK TABLE tokens WRITE"); + &::SendSQL("DELETE FROM tokens WHERE token = $quotedtoken"); + &::SendSQL("UNLOCK TABLES"); +} + +sub HasPasswordToken { + # Returns a password token if the user has one. Otherwise returns 0 (false). + + my ($userid) = @_; + + &::SendSQL("SELECT token FROM tokens WHERE userid = $userid LIMIT 1"); + my ($token) = &::FetchSQLData(); + + return $token; +} + +1; @@ -713,43 +713,54 @@ sub confirm_login { # to a later section. -Joe Robins, 8/3/00 my $enteredlogin = ""; my $realcryptpwd = ""; - if (defined $::FORM{"Bugzilla_login"} && - defined $::FORM{"Bugzilla_password"}) { - - $enteredlogin = $::FORM{"Bugzilla_login"}; - my $enteredpwd = $::FORM{"Bugzilla_password"}; - CheckEmailSyntax($enteredlogin); - - $realcryptpwd = PasswordForLogin($::FORM{"Bugzilla_login"}); - - if (defined $::FORM{"PleaseMailAPassword"}) { - my $realpwd; - if ($realcryptpwd eq "") { - $realpwd = InsertNewUser($enteredlogin, ""); - } else { - SendSQL("select password from profiles where login_name = " . - SqlQuote($enteredlogin)); - $realpwd = FetchOneColumn(); - } - print "Content-type: text/html\n\n"; - PutHeader("Password has been emailed"); - MailPassword($enteredlogin, $realpwd); - PutFooter(); - exit; - } - SendSQL("SELECT encrypt(" . SqlQuote($enteredpwd) . ", " . - SqlQuote(substr($realcryptpwd, 0, 2)) . ")"); - my $enteredcryptpwd = FetchOneColumn(); + # If the form contains Bugzilla login and password fields, use Bugzilla's + # built-in authentication to authenticate the user (otherwise use LDAP below). + if (defined $::FORM{"Bugzilla_login"} && defined $::FORM{"Bugzilla_password"}) { + # Make sure the user's login name is a valid email address. + $enteredlogin = $::FORM{"Bugzilla_login"}; + CheckEmailSyntax($enteredlogin); + + # Retrieve the user's ID and crypted password from the database. + my $userid; + SendSQL("SELECT userid, cryptpassword FROM profiles + WHERE login_name = " . SqlQuote($enteredlogin)); + ($userid, $realcryptpwd) = FetchSQLData(); + + # If this is a new user, generate a password, insert a record + # into the database, and email their password to them. + if ( defined $::FORM{"PleaseMailAPassword"} && !$userid ) { + my $password = InsertNewUser($enteredlogin, ""); + print "Content-Type: text/html\n\n"; + PutHeader("Account Created"); + MailPassword($enteredlogin, $password); + PutFooter(); + exit; + } + + # Otherwise, authenticate the user. + else { + # Get the salt from the user's crypted password. + my $salt = $realcryptpwd; + + # Using the salt, crypt the password the user entered. + my $enteredCryptedPassword = crypt( $::FORM{"Bugzilla_password"} , $salt ); + + # Make sure the passwords match or throw an error. + ($enteredCryptedPassword eq $realcryptpwd) + || DisplayError("The username or password you entered is not valid.") + && exit; + + # If the user has successfully logged in, delete any password tokens + # lying around in the system for them. + use Token; + my $token = Token::HasPasswordToken($userid); + while ( $token ) { + Token::Cancel($token, "user logged in"); + $token = Token::HasPasswordToken($userid); + } + } - if ($realcryptpwd eq "" || $enteredcryptpwd ne $realcryptpwd) { - print "Content-type: text/html\n\n"; - PutHeader("Login failed"); - print "The username or password you entered is not valid.\n"; - print "Please click <b>Back</b> and try again.\n"; - PutFooter(); - exit; - } } elsif (Param("useLDAP") && defined $::FORM{"LDAP_login"} && defined $::FORM{"LDAP_password"}) { @@ -952,23 +963,32 @@ Content-type: text/html </tr> </table> "; - foreach my $i (keys %::FORM) { - if ($i =~ /^Bugzilla_/) { - next; - } - print "<input type=hidden name=$i value=\"@{[value_quote($::FORM{$i})]}\">\n"; + # Add all the form fields into the form as hidden fields + # (except for Bugzilla_login and Bugzilla_password which we + # already added as text fields above). + foreach my $i ( grep( $_ !~ /^Bugzilla_/ , keys %::FORM ) ) { + print qq|<input type="hidden" name="$i" value="@{[value_quote($::FORM{$i})]}">\n|; } - print " -<input type=submit value=Login name=GoAheadAndLogIn><hr> -"; - # If we're using LDAP, we can't request that a password be mailed... - unless(Param("useLDAP")) { - print " -If you don't have a password, or have forgotten it, then please fill in the -e-mail address above and click - here:<input type=submit value=\"E-mail me a password\" -name=PleaseMailAPassword> -</form>\n"; + + print qq| + <input type="submit" name="GoAheadAndLogIn" value="Login"> + </form> + |; + + # Allow the user to request a token to change their password (unless + # we are using LDAP, in which case the user must use LDAP to change it). + unless( Param("useLDAP") ) { + print qq| + <hr> + <form method="get" action="token.cgi"> + <input type="hidden" name="a" value="reqpw"> + If you don't have a password or have forgotten it, + enter your login name below and submit a request + to change your password.<br> + <input size="35" name="loginname"> + <input type="submit" value="Submit Request"> + </form> + |; } # This seems like as good as time as any to get rid of old diff --git a/Token.pm b/Token.pm new file mode 100644 index 000000000..cde97f87e --- /dev/null +++ b/Token.pm @@ -0,0 +1,184 @@ +#!/usr/bonsaitools/bin/perl -w +# -*- Mode: perl; indent-tabs-mode: nil -*- +# +# The contents of this file are subject to the Mozilla Public +# License Version 1.1 (the "License"); you may not use this file +# except in compliance with the License. You may obtain a copy of +# the License at http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS +# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or +# implied. See the License for the specific language governing +# rights and limitations under the License. +# +# The Original Code is the Bugzilla Bug Tracking System. +# +# The Initial Developer of the Original Code is Netscape Communications +# Corporation. Portions created by Netscape are +# Copyright (C) 1998 Netscape Communications Corporation. All +# Rights Reserved. +# +# Contributor(s): Myk Melez <myk@mozilla.org> + +################################################################################ +# Module Initialization +################################################################################ + +# Make it harder for us to do dangerous things in Perl. +use diagnostics; +use strict; + +# Bundle the functions in this file together into the "Token" package. +package Token; + +# This module requires that its caller have said "require CGI.pl" to import +# relevant functions from that script and its companion globals.pl. + +################################################################################ +# Functions +################################################################################ + +sub IssuePasswordToken { + # Generates a random token, adds it to the tokens table, and sends it + # to the user with instructions for using it to change their password. + + my ($loginname) = @_; + + # Retrieve the user's ID from the database. + my $quotedloginname = &::SqlQuote($loginname); + &::SendSQL("SELECT userid FROM profiles WHERE login_name = $quotedloginname"); + my ($userid) = &::FetchSQLData(); + + # Generate a unique token and insert it into the tokens table. + # We have to lock the tokens table before generating the token, + # since the database must be queried for token uniqueness. + &::SendSQL("LOCK TABLE tokens WRITE"); + my $token = GenerateUniqueToken(); + my $quotedtoken = &::SqlQuote($token); + my $quotedipaddr = &::SqlQuote($::ENV{'REMOTE_ADDR'}); + &::SendSQL("INSERT INTO tokens ( userid , issuedate , token , tokentype , eventdata ) + VALUES ( $userid , NOW() , $quotedtoken , 'password' , $quotedipaddr )"); + &::SendSQL("UNLOCK TABLES"); + + # Mail the user the token along with instructions for using it. + MailPasswordToken($loginname, $token); + +} + + +sub GenerateUniqueToken { + # Generates a unique random token. Uses &GenerateRandomPassword + # for the tokens themselves and checks uniqueness by searching for + # the token in the "tokens" table. Gives up if it can't come up + # with a token after about one hundred tries. + + my $token; + my $duplicate = 1; + my $tries = 0; + while ($duplicate) { + + ++$tries; + if ($tries > 100) { + &::DisplayError("Something is seriously wrong with the token generation system."); + exit; + } + + $token = &::GenerateRandomPassword(); + &::SendSQL("SELECT userid FROM tokens WHERE token = " . &::SqlQuote($token)); + $duplicate = &::FetchSQLData(); + } + + return $token; + +} + +sub MailPasswordToken { + # Emails a password token to a user along with instructions for its use. + # Called exclusively from &IssuePasswordToken. + + my ($emailaddress, $token) = @_; + + my $urlbase = &::Param("urlbase"); + my $emailsuffix = &::Param('emailsuffix'); + + open SENDMAIL, "|/usr/lib/sendmail -t"; + + print SENDMAIL qq|From: bugzilla-daemon +To: $emailaddress$emailsuffix +Subject: Bugzilla Change Password Request + +You or someone impersonating you has requested to change your Bugzilla +password. To change your password, visit the following link: + +${urlbase}token.cgi?a=cfmpw&t=$token + +If you are not the person who made this request, or you wish to cancel +this request, visit the following link: + +${urlbase}token.cgi?a=cxlpw&t=$token +|; + close SENDMAIL; +} + +sub Cancel { + # Cancels a previously issued token and notifies the system administrator. + # This should only happen when the user accidentally makes a token request + # or when a malicious hacker makes a token request on behalf of a user. + + my ($token, $cancelaction) = @_; + + # Quote the token for inclusion in SQL statements. + my $quotedtoken = &::SqlQuote($token); + + # Get information about the token being cancelled. + &::SendSQL("SELECT issuedate , tokentype , eventdata , login_name , realname + FROM tokens, profiles + WHERE tokens.userid = profiles.userid + AND token = $quotedtoken"); + my ($issuedate, $tokentype, $eventdata, $loginname, $realname) = &::FetchSQLData(); + + # Get the email address of the Bugzilla maintainer. + my $maintainer = &::Param('maintainer'); + + # Format the user's real name and email address into a single string. + my $username = $realname ? $realname . " <" . $loginname . ">" : $loginname; + + # Notify the user via email about the cancellation. + open SENDMAIL, "|/usr/lib/sendmail -t"; + print SENDMAIL qq|From: bugzilla-daemon +To: $username +Subject: "$tokentype" token cancelled + +A token was cancelled from $::ENV{'REMOTE_ADDR'}. This is either +an honest mistake or the result of a malicious hack attempt. +Take a look at the information below and forward this email +to $maintainer if you suspect foul play. + + Token: $token + Token Type: $tokentype + User: $username + Issue Date: $issuedate + Event Data: $eventdata + +Cancelled Because: $cancelaction +|; + close SENDMAIL; + + # Delete the token from the database. + &::SendSQL("LOCK TABLE tokens WRITE"); + &::SendSQL("DELETE FROM tokens WHERE token = $quotedtoken"); + &::SendSQL("UNLOCK TABLES"); +} + +sub HasPasswordToken { + # Returns a password token if the user has one. Otherwise returns 0 (false). + + my ($userid) = @_; + + &::SendSQL("SELECT token FROM tokens WHERE userid = $userid LIMIT 1"); + my ($token) = &::FetchSQLData(); + + return $token; +} + +1; diff --git a/checksetup.pl b/checksetup.pl index c314bad84..7102df13c 100755 --- a/checksetup.pl +++ b/checksetup.pl @@ -914,7 +914,7 @@ $table{groups} = $table{logincookies} = 'cookie mediumint not null auto_increment primary key, userid mediumint not null, - cryptpassword varchar(64), + cryptpassword varchar(34), hostname varchar(128), lastused timestamp, @@ -936,8 +936,7 @@ $table{products} = $table{profiles} = 'userid mediumint not null auto_increment primary key, login_name varchar(255) not null, - password varchar(16), - cryptpassword varchar(64), + cryptpassword varchar(34), realname varchar(255), groupset bigint not null, disabledtext mediumtext, @@ -1038,6 +1037,19 @@ $table{duplicates} = 'dupe_of mediumint(9) not null, dupe mediumint(9) not null primary key'; +# 2001-06-21, myk@mozilla.org, bug 77473: +# Stores the tokens users receive when they want to change their password +# or email address. Tokens provide an extra measure of security for these changes. +$table{tokens} = + 'userid mediumint not null , + issuedate datetime not null , + token varchar(16) not null primary key , + tokentype varchar(8) not null , + eventdata tinytext null , + + index(userid)'; + + ########################################################################### # Create tables @@ -1417,12 +1429,15 @@ _End_Of_SQL_ system("stty","-echo"); # disable input echoing while( $pass1 ne $pass2 ) { - while( $pass1 eq "" ) { + while( $pass1 eq "" || $pass1 !~ /^[a-zA-Z0-9-_]{3,16}$/ ) { print "Enter a password for the administrator account: "; $pass1 = <STDIN>; chomp $pass1; if(! $pass1 ) { print "\n\nIt's just plain stupid to not have a password. Try again!\n"; + } elsif ( $pass1 !~ /^[a-zA-Z0-9-_]{3,16}$/ ) { + print "The password must be 3-16 characters in length + and contain only letters, numbers, hyphens (-), and underlines (_)."; } } print "\nPlease retype the password to verify: "; @@ -1435,6 +1450,9 @@ _End_Of_SQL_ } } + # Crypt the administrator's password + my $cryptedpassword = Crypt($pass1); + system("stty","echo"); # re-enable input echoing $SIG{HUP} = 'DEFAULT'; # and remove our interrupt hooks $SIG{INT} = 'DEFAULT'; @@ -1442,12 +1460,12 @@ _End_Of_SQL_ $SIG{TERM} = 'DEFAULT'; $realname = $dbh->quote($realname); - $pass1 = $dbh->quote($pass1); + $cryptedpassword = $dbh->quote($cryptedpassword); $dbh->do(<<_End_Of_SQL_); INSERT INTO profiles - (login_name, realname, password, cryptpassword, groupset) - VALUES ($login, $realname, $pass1, encrypt($pass1), 0x7fffffffffffffff) + (login_name, realname, cryptpassword, groupset) + VALUES ($login, $realname, $cryptedpassword, 0x7fffffffffffffff) _End_Of_SQL_ } else { $dbh->do(<<_End_Of_SQL_); @@ -1460,6 +1478,41 @@ _End_Of_SQL_ } +sub Crypt { + # Crypts a password, generating a random salt to do it. + # Random salts are generated because the alternative is usually + # to use the first two characters of the password itself, and since + # the salt appears in plaintext at the beginning of the crypted + # password string this has the effect of revealing the first two + # characters of the password to anyone who views the crypted version. + + my ($password) = @_; + + # The list of characters that can appear in a salt. Salts and hashes + # are both encoded as a sequence of characters from a set containing + # 64 characters, each one of which represents 6 bits of the salt/hash. + # The encoding is similar to BASE64, the difference being that the + # BASE64 plus sign (+) is replaced with a forward slash (/). + my @saltchars = (0..9, 'A'..'Z', 'a'..'z', '.', '/'); + + # Generate the salt. We use an 8 character (48 bit) salt for maximum + # security on systems whose crypt uses MD5. Systems with older + # versions of crypt will just use the first two characters of the salt. + my $salt = ''; + for ( my $i=0 ; $i < 8 ; ++$i ) { + $salt .= $saltchars[rand(64)]; + } + + # Crypt the password. + my $cryptedpassword = crypt($password, $salt); + + # Return the crypted password. + return $cryptedpassword; +} + + + + ########################################################################### # Create initial test product if there are no products present. ########################################################################### @@ -1808,10 +1861,10 @@ if (GetFieldDef('bugs', 'long_desc')) { # him or something. Invent a new profile entry, # disabled, just to represent him. $dbh->do("INSERT INTO profiles " . - "(login_name, password, cryptpassword," . + "(login_name, cryptpassword," . " disabledtext) VALUES (" . $dbh->quote($name) . - ", 'okthen', encrypt('okthen'), " . + ", " . $dbh->quote(Crypt('okthen')) . ", " . "'Account created only to maintain database integrity')"); $s2 = $dbh->prepare("SELECT LAST_INSERT_ID()"); $s2->execute(); @@ -2204,6 +2257,42 @@ if (-d 'shadow') { DropField("profiles", "emailnotification"); DropField("profiles", "newemailtech"); +# 2001-06-12; myk@mozilla.org; bugs 74032, 77473: +# Recrypt passwords using Perl &crypt instead of the mysql equivalent +# and delete plaintext passwords from the database. +if ( GetFieldDef('profiles', 'password') ) { + + print <<ENDTEXT; +Your current installation of Bugzilla stores passwords in plaintext +in the database and uses mysql's encrypt function instead of Perl's +crypt function to crypt passwords. Passwords are now going to be +re-crypted with the Perl function, and plaintext passwords will be +deleted from the database. This could take a while if your +installation has many users. +ENDTEXT + + # Re-crypt everyone's password. + my $sth = $dbh->prepare("SELECT userid, password FROM profiles"); + $sth->execute(); + + my $i = 1; + + print "Fixing password #1... "; + while (my ($userid, $password) = $sth->fetchrow_array()) { + my $cryptpassword = $dbh->quote(Crypt($password)); + $dbh->do("UPDATE profiles SET cryptpassword = $cryptpassword WHERE userid = $userid"); + ++$i; + # Let the user know where we are at every 500 records. + print "$i... " if !($i%500); + } + print "$i... Done.\n"; + + # Drop the plaintext password field and resize the cryptpassword field. + DropField('profiles', 'password'); + ChangeFieldType('profiles', 'cryptpassword', 'varchar(34)'); + +} + # # 2001-06-06 justdave@syndicomm.com: # There was no index on the 'who' column in the long descriptions table. diff --git a/createaccount.cgi b/createaccount.cgi index c2358d6fd..5b9bfb9f6 100755 --- a/createaccount.cgi +++ b/createaccount.cgi @@ -57,13 +57,18 @@ my $realname = $::FORM{'realname'}; if (defined $login) { CheckEmailSyntax($login); if (DBname_to_id($login) != 0) { - PutHeader("Account exists"); - print "A bugzilla account for the name <tt>$login</tt> already\n"; - print "exists. If you have forgotten the password for it, then\n"; - print "<a href=query.cgi?GoAheadAndLogIn>click here</a> and use\n"; - print "the <b>E-mail me a password</b> button.\n"; + PutHeader("Account Exists"); + print qq| + <form method="get" action="token.cgi"> + <input type="hidden" name="a" value="reqpw"> + <input type="hidden" name="loginname" value="$login"> + A Bugzilla account for <tt>$login</tt> already exists. If you + are the account holder and have forgotten your password, + <input type="submit" value="submit a request to change it">. + </form> + |; PutFooter(); - exit; + exit; } PutHeader("Account created"); my $password = InsertNewUser($login, $realname); diff --git a/editusers.cgi b/editusers.cgi index 41240473e..48dcfa0c1 100755 --- a/editusers.cgi +++ b/editusers.cgi @@ -97,10 +97,9 @@ sub EmitElement ($$) # Displays the form to edit a user parameters # -sub EmitFormElements ($$$$$$) +sub EmitFormElements ($$$$$) { - my ($user, $password, $realname, $groupset, $blessgroupset, - $disabledtext) = @_; + my ($user, $realname, $groupset, $blessgroupset, $disabledtext) = @_; print " <TH ALIGN=\"right\">Login name:</TH>\n"; EmitElement("user", $user); @@ -115,7 +114,11 @@ sub EmitFormElements ($$$$$$) if(Param('useLDAP')) { print " <TD><FONT COLOR=RED>This site is using LDAP for authentication!</FONT></TD>\n"; } else { - print " <TD><INPUT TYPE=\"PASSWORD\" SIZE=16 MAXLENGTH=16 NAME=\"password\" VALUE=\"$password\"></TD>\n"; + print qq| + <TD><INPUT TYPE="PASSWORD" SIZE="16" MAXLENGTH="16" NAME="password" VALUE=""><br> + (enter new password to change) + </TD> + |; } print "</TR><TR>\n"; @@ -386,7 +389,7 @@ if ($action eq 'add') { print "<FORM METHOD=POST ACTION=editusers.cgi>\n"; print "<TABLE BORDER=0 CELLPADDING=4 CELLSPACING=0><TR>\n"; - EmitFormElements('', '', '', 0, 0, ''); + EmitFormElements('', '', 0, 0, ''); print "</TR></TABLE>\n<HR>\n"; print "<INPUT TYPE=SUBMIT VALUE=\"Add\">\n"; @@ -423,7 +426,10 @@ if ($action eq 'new') { # Cleanups and valididy checks my $realname = trim($::FORM{realname} || ''); - my $password = trim($::FORM{password} || ''); + # We don't trim the password since that could falsely lead the user + # to believe a password with a space was accepted even though a space + # is an illegal character in a Bugzilla password. + my $password = $::FORM{'password'}; my $disabledtext = trim($::FORM{disabledtext} || ''); my $emailregexp = Param("emailregexp"); @@ -445,10 +451,9 @@ if ($action eq 'new') { PutTrailer($localtrailer); exit; } - if ($password !~ /^[a-zA-Z0-9-_]*$/ || length($password) < 3 || length($password) > 16) { - print "The new user must have a password. The password must be between ", - "3 and 16 characters long and must contain only numbers, letters, ", - "hyphens and underlines. Press <b>Back</b> and try again.\n"; + my $passworderror = ValidatePassword($password); + if ( $passworderror ) { + print $passworderror; PutTrailer($localtrailer); exit; } @@ -473,12 +478,11 @@ if ($action eq 'new') { # Add the new user SendSQL("INSERT INTO profiles ( " . - "login_name, password, cryptpassword, realname, groupset, " . + "login_name, cryptpassword, realname, groupset, " . "disabledtext" . " ) VALUES ( " . SqlQuote($user) . "," . - SqlQuote($password) . "," . - "encrypt(" . SqlQuote($password) . ")," . + SqlQuote(Crypt($password)) . "," . SqlQuote($realname) . "," . $bits . "," . SqlQuote($disabledtext) . ")" ); @@ -682,24 +686,20 @@ if ($action eq 'edit') { CheckUser($user); # get data of user - SendSQL("SELECT password, realname, groupset, blessgroupset, disabledtext + SendSQL("SELECT realname, groupset, blessgroupset, disabledtext FROM profiles WHERE login_name=" . SqlQuote($user)); - my ($password, $realname, $groupset, $blessgroupset, + my ($realname, $groupset, $blessgroupset, $disabledtext) = FetchSQLData(); print "<FORM METHOD=POST ACTION=editusers.cgi>\n"; print "<TABLE BORDER=0 CELLPADDING=4 CELLSPACING=0><TR>\n"; - EmitFormElements($user, $password, $realname, $groupset, $blessgroupset, - $disabledtext); + EmitFormElements($user, $realname, $groupset, $blessgroupset, $disabledtext); print "</TR></TABLE>\n"; print "<INPUT TYPE=HIDDEN NAME=\"userold\" VALUE=\"$user\">\n"; - if ($editall && !Param('useLDAP')) { - print "<INPUT TYPE=HIDDEN NAME=\"passwordold\" VALUE=\"$password\">\n"; - } print "<INPUT TYPE=HIDDEN NAME=\"realnameold\" VALUE=\"$realname\">\n"; print "<INPUT TYPE=HIDDEN NAME=\"groupsetold\" VALUE=\"$groupset\">\n"; print "<INPUT TYPE=HIDDEN NAME=\"blessgroupsetold\" VALUE=\"$blessgroupset\">\n"; @@ -726,8 +726,7 @@ if ($action eq 'update') { my $userold = trim($::FORM{userold} || ''); my $realname = trim($::FORM{realname} || ''); my $realnameold = trim($::FORM{realnameold} || ''); - my $password = trim($::FORM{password} || ''); - my $passwordold = trim($::FORM{passwordold} || ''); + my $password = $::FORM{password} || ''; my $disabledtext = trim($::FORM{disabledtext} || ''); my $disabledtextold = trim($::FORM{disabledtextold} || ''); my $groupsetold = trim($::FORM{groupsetold} || '0'); @@ -791,14 +790,19 @@ if ($action eq 'update') { print "Updated ability to tweak permissions of other users.\n"; } - if(!Param('useLDAP')) { - if ($editall && $password ne $passwordold) { - my $q = SqlQuote($password); - SendSQL("UPDATE profiles - SET password= $q, cryptpassword = ENCRYPT($q) - WHERE login_name=" . SqlQuote($userold)); - print "Updated password.<BR>\n"; - } + # Update the database with the user's new password if they changed it. + if ( !Param('useLDAP') && $editall && $password ) { + my $passworderror = ValidatePassword($password); + if ( !$passworderror ) { + my $cryptpassword = SqlQuote(Crypt($password)); + my $loginname = SqlQuote($userold); + SendSQL("UPDATE profiles + SET cryptpassword = $cryptpassword + WHERE login_name = $loginname"); + print "Updated password.<BR>\n"; + } else { + print "Did not update password: $passworderror<br>\n"; + } } if ($editall && $realname ne $realnameold) { SendSQL("UPDATE profiles diff --git a/globals.pl b/globals.pl index 736cb431a..302d9b8b7 100644 --- a/globals.pl +++ b/globals.pl @@ -616,19 +616,21 @@ sub GetVersionTable { sub InsertNewUser { my ($username, $realname) = (@_); - my $password = ""; - for (my $i=0 ; $i<8 ; $i++) { - $password .= substr("abcdefghijklmnopqrstuvwxyz", int(rand(26)), 1); - } + # Generate a new random password for the user. + my $password = GenerateRandomPassword(); + my $cryptpassword = Crypt($password); + + # Determine what groups the user should be in by default + # and add them to those groups. PushGlobalSQLState(); SendSQL("select bit, userregexp from groups where userregexp != ''"); my $groupset = "0"; while (MoreSQLData()) { my @row = FetchSQLData(); - # Modified -Joe Robins, 2/17/00 - # Making this case insensitive, since usernames are email addresses, - # and could be any case. + # Modified -Joe Robins, 2/17/00 + # Making this case insensitive, since usernames are email addresses, + # and could be any case. if ($username =~ m/$row[1]/i) { $groupset .= "+ $row[0]"; # Silly hack to let MySQL do the math, # not Perl, since we're dealing with 64 @@ -636,14 +638,103 @@ sub InsertNewUser { # does that. } } - + + # Insert the new user record into the database. $username = SqlQuote($username); $realname = SqlQuote($realname); - SendSQL("insert into profiles (login_name, realname, password, cryptpassword, groupset) values ($username, $realname, '$password', encrypt('$password'), $groupset)"); + $cryptpassword = SqlQuote($cryptpassword); + SendSQL("INSERT INTO profiles (login_name, realname, cryptpassword, groupset) + VALUES ($username, $realname, $cryptpassword, $groupset)"); PopGlobalSQLState(); + + # Return the password to the calling code so it can be included + # in an email sent to the user. + return $password; +} + +sub GenerateRandomPassword { + my ($size) = @_; + + # Generated passwords are eight characters long by default. + $size ||= 8; + + # The list of characters that can appear in a password. + # If you change this you must also update &ValidatePassword below. + my @pwchars = (0..9, 'A'..'Z', 'a'..'z', '-', '_'); + #my @pwchars = (0..9, 'A'..'Z', 'a'..'z', '-', '_', '!', '@', '#', '$', '%', '^', '&', '*'); + + # The number of characters in the list. + my $pwcharslen = scalar(@pwchars); + + # Generate the password. + my $password = ""; + for ( my $i=0 ; $i<$size ; $i++ ) { + $password .= $pwchars[rand($pwcharslen)]; + } + + # Return the password. return $password; } + +sub ValidatePassword { + # Determines whether or not a password is valid (i.e. meets Bugzilla's + # requirements for length and content). If the password is valid, the + # function returns boolean false. Otherwise it returns an error message + # (synonymous with boolean true) that can be displayed to the user. + + # If a second password is passed in, this function also verifies that + # the two passwords match. + + my ($password, $matchpassword) = @_; + + if ( $password !~ /^[a-zA-Z0-9-_]*$/ ) { + return "The password contains an illegal character. Legal characters are letters, numbers, hyphens (-), and underlines (_)."; + } elsif ( length($password) < 3 ) { + return "The password is less than three characters long. It must be at least three characters."; + } elsif ( length($password) > 16 ) { + return "The password is more than 16 characters long. It must be no more than 16 characters."; + } elsif ( $matchpassword && $password ne $matchpassword ) { + return "The two passwords do not match."; + } + + return 0; +} + + +sub Crypt { + # Crypts a password, generating a random salt to do it. + # Random salts are generated because the alternative is usually + # to use the first two characters of the password itself, and since + # the salt appears in plaintext at the beginning of the crypted + # password string this has the effect of revealing the first two + # characters of the password to anyone who views the crypted version. + + my ($password) = @_; + + # The list of characters that can appear in a salt. Salts and hashes + # are both encoded as a sequence of characters from a set containing + # 64 characters, each one of which represents 6 bits of the salt/hash. + # The encoding is similar to BASE64, the difference being that the + # BASE64 plus sign (+) is replaced with a forward slash (/). + my @saltchars = (0..9, 'A'..'Z', 'a'..'z', '.', '/'); + + # Generate the salt. We use an 8 character (48 bit) salt for maximum + # security on systems whose crypt uses MD5. Systems with older + # versions of crypt will just use the first two characters of the salt. + my $salt = ''; + for ( my $i=0 ; $i < 8 ; ++$i ) { + $salt .= $saltchars[rand(64)]; + } + + # Crypt the password. + my $cryptedpassword = crypt($password, $salt); + + # Return the crypted password. + return $cryptedpassword; +} + + sub DBID_to_real_or_loginname { my ($id) = (@_); PushGlobalSQLState(); diff --git a/sanitycheck.cgi b/sanitycheck.cgi index bc3b823c7..c8f628e58 100755 --- a/sanitycheck.cgi +++ b/sanitycheck.cgi @@ -165,25 +165,6 @@ CrossCheck("profiles", "userid", ["components", "initialowner", "value"], ["components", "initialqacontact", "value", ["0"]]); -Status("Checking passwords"); -SendSQL("SELECT COUNT(*) FROM profiles WHERE cryptpassword != ENCRYPT(password, left(cryptpassword, 2))"); -my $count = FetchOneColumn(); -if ($count) { - Alert("$count entries have problems in their crypted password."); - if ($::FORM{'rebuildpasswords'}) { - Status("Rebuilding passwords"); - SendSQL("UPDATE profiles - SET cryptpassword = ENCRYPT(password, - left(cryptpassword, 2)) - WHERE cryptpassword != ENCRYPT(password, - left(cryptpassword, 2))"); - Status("Passwords have been rebuilt."); - } else { - print qq{<a href="sanitycheck.cgi?rebuildpasswords=1">Click here to rebuild the crypted passwords</a><p>\n}; - } -} - - Status("Checking groups"); SendSQL("select bit from groups where bit != pow(2, round(log(bit) / log(2)))"); diff --git a/token.cgi b/token.cgi new file mode 100755 index 000000000..145f7d6f8 --- /dev/null +++ b/token.cgi @@ -0,0 +1,243 @@ +#!/usr/bonsaitools/bin/perl -w +# -*- Mode: perl; indent-tabs-mode: nil -*- +# +# The contents of this file are subject to the Mozilla Public +# License Version 1.1 (the "License"); you may not use this file +# except in compliance with the License. You may obtain a copy of +# the License at http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS +# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or +# implied. See the License for the specific language governing +# rights and limitations under the License. +# +# The Original Code is the Bugzilla Bug Tracking System. +# +# The Initial Developer of the Original Code is Netscape Communications +# Corporation. Portions created by Netscape are +# Copyright (C) 1998 Netscape Communications Corporation. All +# Rights Reserved. +# +# Contributor(s): Myk Melez <myk@mozilla.org> + +############################################################################ +# Script Initialization +############################################################################ + +# Make it harder for us to do dangerous things in Perl. +use diagnostics; +use strict; + +# Include the Bugzilla CGI and general utility library. +require "CGI.pl"; + +# Establish a connection to the database backend. +ConnectToDatabase(); + +# Use the "Token" module that contains functions for doing various +# token-related tasks. +use Token; + +################################################################################ +# Data Validation / Security Authorization +################################################################################ + +# Throw an error if the form does not contain an "action" field specifying +# what the user wants to do. +$::FORM{'a'} + || DisplayError("I could not figure out what you wanted to do.") + && exit; + +# Assign the action to a global variable. +$::action = $::FORM{'a'}; + +# If a token was submitted, make sure it is a valid token that exists in the +# database and is the correct type for the action being taken. +if ($::FORM{'t'}) { + # Assign the token and its SQL quoted equivalent to global variables. + $::token = $::FORM{'t'}; + $::quotedtoken = SqlQuote($::token); + + # Make sure the token contains only valid characters in the right amount. + my $validationerror = ValidatePassword($::token); + if ($validationerror) { + DisplayError('The token you entered is invalid.'); + exit; + } + + # Make sure the token exists in the database. + SendSQL( "SELECT tokentype FROM tokens WHERE token = $::quotedtoken" ); + (my $tokentype = FetchSQLData()) + || DisplayError("The token you submitted does not exist.") + && exit; + + # Make sure the token is the correct type for the action being taken. + if ( grep($::action eq $_ , qw(cfmpw cxlpw chgpw)) && $tokentype ne 'password' ) { + DisplayError("That token cannot be used to change your password."); + Token::Cancel($::token, "user tried to use token to change password"); + exit; + } +} + +# If the user is requesting a password change, make sure they submitted +# their login name and it exists in the database. +if ( $::action eq 'reqpw' ) { + defined $::FORM{'loginname'} + || DisplayError("You must enter a login name when requesting to change your password.") + && exit; + + # Make sure the login name looks like an email address. This function + # displays its own error and stops execution if the login name looks wrong. + CheckEmailSyntax($::FORM{'loginname'}); + + my $quotedloginname = SqlQuote($::FORM{'loginname'}); + SendSQL("SELECT userid FROM profiles WHERE login_name = $quotedloginname"); + FetchSQLData() + || DisplayError("There is no Bugzilla account with that login name.") + && exit; +} + +# If the user is changing their password, make sure they submitted a new +# password and that the new password is valid. +if ( $::action eq 'chgpw' ) { + defined $::FORM{'password'} + && defined $::FORM{'matchpassword'} + || DisplayError("You cannot change your password without submitting a new one.") + && exit; + + my $passworderror = ValidatePassword($::FORM{'password'}, $::FORM{'matchpassword'}); + if ( $passworderror ) { + DisplayError($passworderror); + exit; + } +} + +################################################################################ +# Main Body Execution +################################################################################ + +# All calls to this script should contain an "action" variable whose value +# determines what the user wants to do. The code below checks the value of +# that variable and runs the appropriate code. + +if ($::action eq 'reqpw') { + requestChangePassword(); +} elsif ($::action eq 'cfmpw') { + confirmChangePassword(); +} elsif ($::action eq 'cxlpw') { + cancelChangePassword(); +} elsif ($::action eq 'chgpw') { + changePassword(); +} else { + # If the action that the user wants to take (specified in the "a" form field) + # is none of the above listed actions, display an error telling the user + # that we do not understand what they would like to do. + DisplayError("I could not figure out what you wanted to do."); +} + +exit; + +################################################################################ +# Functions +################################################################################ + +sub requestChangePassword { + + Token::IssuePasswordToken($::FORM{'loginname'}); + + # Return HTTP response headers. + print "Content-Type: text/html\n\n"; + + PutHeader("Request to Change Password"); + print qq| + <p> + A token for changing your password has been emailed to you. + Follow the instructions in that email to change your password. + </p> + |; + PutFooter(); +} + +sub confirmChangePassword { + + # Return HTTP response headers. + print "Content-Type: text/html\n\n"; + + PutHeader("Change Password"); + print qq| + <p> + To change your password, enter a new password twice: + </p> + <form method="post" action="token.cgi"> + <input type="hidden" name="t" value="$::token"> + <input type="hidden" name="a" value="chgpw"> + <table> + <tr> + <th align="right">New Password:</th> + <td><input type="password" name="password" size="16" maxlength="16"></td> + </tr> + <tr> + <th align="right">New Password Again:</th> + <td><input type="password" name="matchpassword" size="16" maxlength="16"></td> + </tr> + <tr> + <th align="right"> </th> + <td><input type="submit" value="Submit"></td> + </tr> + </table> + </form> + |; + PutFooter(); +} + +sub cancelChangePassword { + + Token::Cancel($::token, "user requested cancellation"); + + # Return HTTP response headers. + print "Content-Type: text/html\n\n"; + + PutHeader("Cancel Request to Change Password"); + print qq| + <p> + Your request has been cancelled. + </p> + |; + PutFooter(); +} + +sub changePassword { + + # Quote the password and token for inclusion into SQL statements. + my $cryptedpassword = Crypt($::FORM{'password'}); + my $quotedpassword = SqlQuote($cryptedpassword); + + # Get the user's ID from the tokens table. + SendSQL("SELECT userid FROM tokens WHERE token = $::quotedtoken"); + my $userid = FetchSQLData(); + + # Update the user's password in the profiles table and delete the token + # from the tokens table. + SendSQL("LOCK TABLE profiles WRITE , tokens WRITE"); + SendSQL("UPDATE profiles + SET cryptpassword = $quotedpassword + WHERE userid = $userid"); + SendSQL("DELETE FROM tokens WHERE token = $::quotedtoken"); + SendSQL("UNLOCK TABLES"); + + # Return HTTP response headers. + print "Content-Type: text/html\n\n"; + + # Let the user know their password has been changed. + PutHeader("Password Changed"); + print qq| + <p> + Your password has been changed. + </p> + |; + PutFooter(); +} + + + + diff --git a/userprefs.cgi b/userprefs.cgi index f880cf8e2..0eeda0e71 100755 --- a/userprefs.cgi +++ b/userprefs.cgi @@ -148,9 +148,12 @@ sub SaveAccount { my $old = SqlQuote($::FORM{'Bugzilla_password'}); my $pwd1 = SqlQuote($::FORM{'pwd1'}); my $pwd2 = SqlQuote($::FORM{'pwd2'}); - SendSQL("SELECT cryptpassword = ENCRYPT($old, LEFT(cryptpassword, 2)) " . - "FROM profiles WHERE userid = $userid"); - if (!FetchOneColumn()) { + SendSQL("SELECT cryptpassword FROM profiles WHERE userid = $userid"); + my $oldcryptedpwd = FetchOneColumn(); + if ( !$oldcryptedpwd ) { + Error("I was unable to retrieve your old password from the database."); + } + if ( crypt($::FORM{'Bugzilla_password'}, $oldcryptedpwd) ne $oldcryptedpwd ) { Error("You did not enter your old password correctly."); } if ($pwd1 ne $pwd2) { @@ -159,9 +162,13 @@ sub SaveAccount { if ($::FORM{'pwd1'} eq '') { Error("You must enter a new password."); } - SendSQL("UPDATE profiles SET password = $pwd1, " . - "cryptpassword = ENCRYPT($pwd1) " . - "WHERE userid = $userid"); + my $passworderror = ValidatePassword($::FORM{'pwd1'}); + Error($passworderror) if $passworderror; + + my $cryptedpassword = SqlQuote(Crypt($::FORM{'pwd1'})); + SendSQL("UPDATE profiles + SET cryptpassword = $cryptedpassword + WHERE userid = $userid"); } SendSQL("UPDATE profiles SET " . "realname = " . SqlQuote($::FORM{'realname'}) . |