From e9de4983bfbf4ad8cd13444633731e32997fa169 Mon Sep 17 00:00:00 2001 From: Tim Starling Date: Mon, 23 Oct 2006 09:35:30 +0000 Subject: [PATCH] Added a per-user limit on password reminder emails. Presumably if you just had a password reminder sent to you, you don't need another one a short time later. Along with a proper per-IP throttle setting, this should fix the majority of the abuse problem on Wikipedia. --- includes/DefaultSettings.php | 6 ++++ includes/SpecialUserlogin.php | 21 +++++++---- includes/User.php | 36 ++++++++++++++++--- languages/messages/MessagesEn.php | 33 ++++++++++++++--- .../archives/patch-user_newpass_time.sql | 4 +++ maintenance/mysql5/tables.sql | 4 +++ maintenance/postgres/tables.sql | 1 + maintenance/tables.sql | 4 +++ maintenance/updaters.inc | 1 + 9 files changed, 94 insertions(+), 16 deletions(-) create mode 100644 maintenance/archives/patch-user_newpass_time.sql diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index 20f4aa700f..21a1cfbf11 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -424,6 +424,12 @@ $wgEnableEmail = true; */ $wgEnableUserEmail = true; +/** + * Minimum time, in hours, which must elapse between password reminder + * emails for a given account. This is to prevent abuse by mail flooding. + */ +$wgPasswordReminderResendTime = 24; + /** * SMTP Mode * For using a direct (authenticated) SMTP server connection. diff --git a/includes/SpecialUserlogin.php b/includes/SpecialUserlogin.php index 1f56141e3f..2cd14687f2 100644 --- a/includes/SpecialUserlogin.php +++ b/includes/SpecialUserlogin.php @@ -123,7 +123,7 @@ class LoginForm { } $u->saveSettings(); - $result = $this->mailPasswordInternal($u); + $result = $this->mailPasswordInternal( $u, false ); wfRunHooks( 'AddNewAccount', array( $u ) ); @@ -343,7 +343,7 @@ class LoginForm { return self::NOT_EXISTS; } } else { - $u->loadFromDatabase(); + $u->load(); } if (!$u->checkPassword( $this->mPassword )) { @@ -419,7 +419,7 @@ class LoginForm { $wgOut->rateLimited(); return; } - + if ( '' == $this->mName ) { $this->mainLoginForm( wfMsg( 'noname' ) ); return; @@ -434,9 +434,16 @@ class LoginForm { return; } - $u->loadFromDatabase(); + # Check against password throttle + if ( $u->isPasswordReminderThrottled() ) { + global $wgPasswordReminderResendTime; + # Round the time in hours to 3 d.p., in case someone is specifying minutes or seconds. + $this->mainLoginForm( wfMsg( 'throttled-mailpassword', + round( $wgPasswordReminderResendTime, 3 ) ) ); + return; + } - $result = $this->mailPasswordInternal( $u ); + $result = $this->mailPasswordInternal( $u, true ); if( WikiError::isError( $result ) ) { $this->mainLoginForm( wfMsg( 'mailerror', $result->getMessage() ) ); } else { @@ -449,7 +456,7 @@ class LoginForm { * @return mixed true on success, WikiError on failure * @private */ - function mailPasswordInternal( $u ) { + function mailPasswordInternal( $u, $throttle = true ) { global $wgCookiePath, $wgCookieDomain, $wgCookiePrefix, $wgCookieSecure; global $wgServer, $wgScript; @@ -458,7 +465,7 @@ class LoginForm { } $np = $u->randomPassword(); - $u->setNewpassword( $np ); + $u->setNewpassword( $np, $throttle ); setcookie( "{$wgCookiePrefix}Token", '', time() - 3600, $wgCookiePath, $wgCookieDomain, $wgCookieSecure ); diff --git a/includes/User.php b/includes/User.php index 4fac12af1f..5d169e5b60 100644 --- a/includes/User.php +++ b/includes/User.php @@ -70,6 +70,7 @@ class User { 'mRealName', 'mPassword', 'mNewpassword', + 'mNewpassTime', 'mEmail', 'mOptions', 'mTouched', @@ -86,9 +87,9 @@ class User { /** * The cache variable declarations */ - var $mId, $mName, $mRealName, $mPassword, $mNewpassword, $mEmail, $mOptions, - $mTouched, $mToken, $mEmailAuthenticated, $mEmailToken, $mEmailTokenExpires, - $mRegistration, $mGroups; + var $mId, $mName, $mRealName, $mPassword, $mNewpassword, $mNewpassTime, + $mEmail, $mOptions, $mTouched, $mToken, $mEmailAuthenticated, + $mEmailToken, $mEmailTokenExpires, $mRegistration, $mGroups; /** * Whether the cache variables have been loaded @@ -574,6 +575,7 @@ class User { $this->mName = $name; $this->mRealName = ''; $this->mPassword = $this->mNewpassword = ''; + $this->mNewpassTime = null; $this->mEmail = ''; $this->mOptions = null; # Defer init @@ -691,6 +693,7 @@ class User { $this->mRealName = $s->user_real_name; $this->mPassword = $s->user_password; $this->mNewpassword = $s->user_newpassword; + $this->mNewpassTime = wfTimestampOrNull( TS_MW, $s->user_newpass_time ); $this->mEmail = $s->user_email; $this->decodeOptions( $s->user_options ); $this->mTouched = wfTimestamp(TS_MW,$s->user_touched); @@ -1285,6 +1288,7 @@ class User { $this->setToken(); $this->mPassword = $this->encryptPassword( $str ); $this->mNewpassword = ''; + $this->mNewpassTime = NULL; } /** @@ -1314,11 +1318,32 @@ class User { $this->mCookiePassword = md5( $str ); } - function setNewpassword( $str ) { + /** + * Set the password for a password reminder or new account email + * Sets the user_newpass_time field if $throttle is true + */ + function setNewpassword( $str, $throttle = true ) { $this->load(); $this->mNewpassword = $this->encryptPassword( $str ); + if ( $throttle ) { + $this->mNewpassTime = wfTimestampNow(); + } } + /** + * Returns true if a password reminder email has already been sent within + * the last $wgPasswordReminderResendTime hours + */ + function isPasswordReminderThrottled() { + global $wgPasswordReminderResendTime; + $this->load(); + if ( !$this->mNewpassTime || !$wgPasswordReminderResendTime ) { + return false; + } + $expiry = wfTimestamp( TS_UNIX, $this->mNewpassTime ) + $wgPasswordReminderResendTime * 3600; + return time() < $expiry; + } + function getEmail() { $this->load(); return $this->mEmail; @@ -1774,6 +1799,7 @@ class User { 'user_name' => $this->mName, 'user_password' => $this->mPassword, 'user_newpassword' => $this->mNewpassword, + 'user_newpass_time' => $dbw->timestampOrNull( $this->mNewpassTime ), 'user_real_name' => $this->mRealName, 'user_email' => $this->mEmail, 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ), @@ -1834,6 +1860,7 @@ class User { 'user_name' => $name, 'user_password' => $user->mPassword, 'user_newpassword' => $user->mNewpassword, + 'user_newpass_time' => $dbw->timestamp( $user->mNewpassTime ), 'user_email' => $user->mEmail, 'user_email_authenticated' => $dbw->timestampOrNull( $user->mEmailAuthenticated ), 'user_real_name' => $user->mRealName, @@ -1866,6 +1893,7 @@ class User { 'user_name' => $this->mName, 'user_password' => $this->mPassword, 'user_newpassword' => $this->mNewpassword, + 'user_newpass_time' => $dbw->timestamp( $this->mNewpassTime ), 'user_email' => $this->mEmail, 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ), 'user_real_name' => $this->mRealName, diff --git a/languages/messages/MessagesEn.php b/languages/messages/MessagesEn.php index a457dabfc6..0f1d71da19 100644 --- a/languages/messages/MessagesEn.php +++ b/languages/messages/MessagesEn.php @@ -144,8 +144,18 @@ $datePreferences = array( 'ISO 8601', ); +/** + * The date format to use for generated dates in the user interface. + * This may be one of the above date preferences, or the special value + * "dmy or mdy", which uses mdy if $wgAmericanDates is true, and dmy + * if $wgAmericanDates is false. + */ $defaultDateFormat = 'dmy or mdy'; +/** + * Associative array mapping old numeric date formats, which may still be + * stored in user preferences, to the new string formats. + */ $datePreferenceMigrationMap = array( 'default', 'mdy', @@ -179,6 +189,9 @@ $dateFormats = array( 'ISO 8601 both' => 'xnY-xnm-xnd"T"xnH:xni:xns', ); +/** + * Default list of book sources + */ $bookstoreList = array( 'AddALL' => 'http://www.addall.com/New/Partner.cgi?query=$1&type=ISBN', 'PriceSCAN' => 'http://www.pricescan.com/books/bookDetail.asp?isbn=$1', @@ -186,10 +199,14 @@ $bookstoreList = array( 'Amazon.com' => 'http://www.amazon.com/exec/obidos/ISBN=$1' ); -# Note to translators: -# Please include the English words as synonyms. This allows people -# from other wikis to contribute more easily. -# +/** + * Magic words + * Customisable syntax for wikitext and elsewhere + * + * Note to translators: + * Please include the English words as synonyms. This allows people + * from other wikis to contribute more easily. + */ $magicWords = array( # ID CASE SYNONYMS 'redirect' => array( 0, '#REDIRECT' ), @@ -299,9 +316,12 @@ $magicWords = array( 'formatnum' => array( 0, 'FORMATNUM' ), 'padleft' => array( 0, 'PADLEFT' ), 'padright' => array( 0, 'PADRIGHT' ), - ); +/** + * Regular expression matching the "link trail", e.g. "ed" in [[Toast]]ed, as + * the first group, and the remainder of the string as the second group. + */ $linkTrail = '/^([a-z]+)(.*)$/sD'; #------------------------------------------------------------------- @@ -736,6 +756,9 @@ is not allowed to use the password recovery function to prevent abuse.', 'eauthentsent' => 'A confirmation e-mail has been sent to the nominated e-mail address. Before any other mail is sent to the account, you will have to follow the instructions in the e-mail, to confirm that the account is actually yours.', +'throttled-mailpassword' => 'A password reminder has already been sent, within the +last $1 hours. To prevent abuse, only one password reminder will be sent per +$1 hours.', 'loginend' => '', 'signupend' => '{{int:loginend}}', 'mailerror' => 'Error sending mail: $1', diff --git a/maintenance/archives/patch-user_newpass_time.sql b/maintenance/archives/patch-user_newpass_time.sql new file mode 100644 index 0000000000..befbf0ef5a --- /dev/null +++ b/maintenance/archives/patch-user_newpass_time.sql @@ -0,0 +1,4 @@ +-- Timestamp of the last time when a new password was +-- sent, for throttling purposes +ALTER TABLE user ADD user_newpass_time char(14) binary; + diff --git a/maintenance/mysql5/tables.sql b/maintenance/mysql5/tables.sql index 81a4690adc..9682ecd21e 100644 --- a/maintenance/mysql5/tables.sql +++ b/maintenance/mysql5/tables.sql @@ -86,6 +86,10 @@ CREATE TABLE /*$wgDBprefix*/user ( -- at which point the hash is moved to user_password -- and the old password is invalidated. user_newpassword tinyblob NOT NULL default '', + + -- Timestamp of the last time when a new password was + -- sent, for throttling purposes + user_newpass_time char(14) binary, -- Note: email should be restricted, not public info. -- Same with passwords. diff --git a/maintenance/postgres/tables.sql b/maintenance/postgres/tables.sql index 8ee8720335..8534a1bdae 100644 --- a/maintenance/postgres/tables.sql +++ b/maintenance/postgres/tables.sql @@ -17,6 +17,7 @@ CREATE TABLE mwuser ( -- replace reserved word 'user' user_real_name TEXT, user_password TEXT, user_newpassword TEXT, + user_newpass_time TIMESTAMPTZ, user_token CHAR(32), user_email TEXT, user_email_token CHAR(32), diff --git a/maintenance/tables.sql b/maintenance/tables.sql index 3ffa5e5f66..9b2955d3e0 100644 --- a/maintenance/tables.sql +++ b/maintenance/tables.sql @@ -74,6 +74,10 @@ CREATE TABLE /*$wgDBprefix*/user ( -- and the old password is invalidated. user_newpassword tinyblob NOT NULL default '', + -- Timestamp of the last time when a new password was + -- sent, for throttling purposes + user_newpass_time char(14) binary, + -- Note: email should be restricted, not public info. -- Same with passwords. user_email tinytext NOT NULL default '', diff --git a/maintenance/updaters.inc b/maintenance/updaters.inc index d334660e0a..a63307b984 100644 --- a/maintenance/updaters.inc +++ b/maintenance/updaters.inc @@ -57,6 +57,7 @@ $wgNewFields = array( array( 'ipblocks', 'ipb_range_start', 'patch-ipb_range_start.sql' ), array( 'site_stats', 'ss_images', 'patch-ss_images.sql' ), array( 'ipblocks', 'ipb_anon_only', 'patch-ipb_anon_only.sql' ), + array( 'user', 'user_newpass_time','patch-user_newpass_time.sql' ), ); function rename_table( $from, $to, $patch ) { -- 2.20.1