| Index: trunk/phase3/includes/User.php |
| — | — | @@ -2850,7 +2850,7 @@ |
| 2851 | 2851 | return EDIT_TOKEN_SUFFIX; |
| 2852 | 2852 | } else { |
| 2853 | 2853 | if( !isset( $_SESSION['wsEditToken'] ) ) { |
| 2854 | | - $token = $this->generateToken(); |
| | 2854 | + $token = self::generateToken(); |
| 2855 | 2855 | $_SESSION['wsEditToken'] = $token; |
| 2856 | 2856 | } else { |
| 2857 | 2857 | $token = $_SESSION['wsEditToken']; |
| — | — | @@ -2868,7 +2868,7 @@ |
| 2869 | 2869 | * @param $salt \string Optional salt value |
| 2870 | 2870 | * @return \string The new random token |
| 2871 | 2871 | */ |
| 2872 | | - function generateToken( $salt = '' ) { |
| | 2872 | + public static function generateToken( $salt = '' ) { |
| 2873 | 2873 | $token = dechex( mt_rand() ) . dechex( mt_rand() ); |
| 2874 | 2874 | return md5( $token . $salt ); |
| 2875 | 2875 | } |
| — | — | @@ -2967,7 +2967,7 @@ |
| 2968 | 2968 | $now = time(); |
| 2969 | 2969 | $expires = $now + 7 * 24 * 60 * 60; |
| 2970 | 2970 | $expiration = wfTimestamp( TS_MW, $expires ); |
| 2971 | | - $token = $this->generateToken( $this->mId . $this->mEmail . $expires ); |
| | 2971 | + $token = self::generateToken( $this->mId . $this->mEmail . $expires ); |
| 2972 | 2972 | $hash = md5( $token ); |
| 2973 | 2973 | $this->load(); |
| 2974 | 2974 | $this->mEmailToken = $hash; |
| Index: trunk/phase3/includes/api/ApiLogin.php |
| — | — | @@ -58,6 +58,7 @@ |
| 59 | 59 | 'wpName' => $params['name'], |
| 60 | 60 | 'wpPassword' => $params['password'], |
| 61 | 61 | 'wpDomain' => $params['domain'], |
| | 62 | + 'wpLoginToken' => $params['token'], |
| 62 | 63 | 'wpRemember' => '' |
| 63 | 64 | ) ); |
| 64 | 65 | |
| — | — | @@ -86,6 +87,15 @@ |
| 87 | 88 | $result['cookieprefix'] = $wgCookiePrefix; |
| 88 | 89 | $result['sessionid'] = session_id(); |
| 89 | 90 | break; |
| | 91 | + |
| | 92 | + case LoginForm::NEED_TOKEN: |
| | 93 | + $result['result'] = 'NeedToken'; |
| | 94 | + $result['token'] = $loginForm->getLoginToken(); |
| | 95 | + break; |
| | 96 | + |
| | 97 | + case LoginForm::WRONG_TOKEN: |
| | 98 | + $result['result'] = 'WrongToken'; |
| | 99 | + break; |
| 90 | 100 | |
| 91 | 101 | case LoginForm::NO_NAME: |
| 92 | 102 | $result['result'] = 'NoName'; |
| — | — | @@ -146,7 +156,8 @@ |
| 147 | 157 | return array( |
| 148 | 158 | 'name' => null, |
| 149 | 159 | 'password' => null, |
| 150 | | - 'domain' => null |
| | 160 | + 'domain' => null, |
| | 161 | + 'token' => null, |
| 151 | 162 | ); |
| 152 | 163 | } |
| 153 | 164 | |
| — | — | @@ -154,7 +165,8 @@ |
| 155 | 166 | return array( |
| 156 | 167 | 'name' => 'User Name', |
| 157 | 168 | 'password' => 'Password', |
| 158 | | - 'domain' => 'Domain (optional)' |
| | 169 | + 'domain' => 'Domain (optional)', |
| | 170 | + 'token' => 'Login token obtained in first request', |
| 159 | 171 | ); |
| 160 | 172 | } |
| 161 | 173 | |
| — | — | @@ -170,6 +182,8 @@ |
| 171 | 183 | |
| 172 | 184 | public function getPossibleErrors() { |
| 173 | 185 | return array_merge( parent::getPossibleErrors(), array( |
| | 186 | + array( 'code' => 'NeedToken', 'info' => 'You need to resubmit your login with the specified token. See https://bugzilla.wikimedia.org/show_bug.cgi?id=23076' ), |
| | 187 | + array( 'code' => 'WrongToken', 'info' => 'You specified an invalid token' ), |
| 174 | 188 | array( 'code' => 'NoName', 'info' => 'You didn\'t set the lgname parameter' ), |
| 175 | 189 | array( 'code' => 'Illegal', 'info' => ' You provided an illegal username' ), |
| 176 | 190 | array( 'code' => 'NotExists', 'info' => ' The username you provided doesn\'t exist' ), |
| Index: trunk/phase3/includes/specials/SpecialUserlogin.php |
| — | — | @@ -35,11 +35,13 @@ |
| 36 | 36 | const CREATE_BLOCKED = 9; |
| 37 | 37 | const THROTTLED = 10; |
| 38 | 38 | const USER_BLOCKED = 11; |
| | 39 | + const NEED_TOKEN = 12; |
| | 40 | + const WRONG_TOKEN = 13; |
| 39 | 41 | |
| 40 | 42 | var $mName, $mPassword, $mRetype, $mReturnTo, $mCookieCheck, $mPosted; |
| 41 | 43 | var $mAction, $mCreateaccount, $mCreateaccountMail, $mMailmypassword; |
| 42 | 44 | var $mLoginattempt, $mRemember, $mEmail, $mDomain, $mLanguage; |
| 43 | | - var $mSkipCookieCheck, $mReturnToQuery; |
| | 45 | + var $mSkipCookieCheck, $mReturnToQuery, $mToken; |
| 44 | 46 | |
| 45 | 47 | private $mExtUser = null; |
| 46 | 48 | |
| — | — | @@ -70,6 +72,7 @@ |
| 71 | 73 | $this->mRemember = $request->getCheck( 'wpRemember' ); |
| 72 | 74 | $this->mLanguage = $request->getText( 'uselang' ); |
| 73 | 75 | $this->mSkipCookieCheck = $request->getCheck( 'wpSkipCookieCheck' ); |
| | 76 | + $this->mToken = $request->getVal( 'wpLoginToken' ); |
| 74 | 77 | |
| 75 | 78 | if ( $wgRedirectOnLogin ) { |
| 76 | 79 | $this->mReturnTo = $wgRedirectOnLogin; |
| — | — | @@ -395,6 +398,21 @@ |
| 396 | 399 | return self::NO_NAME; |
| 397 | 400 | } |
| 398 | 401 | |
| | 402 | + // We require a login token to prevent login CSRF |
| | 403 | + // Handle part of this before incrementing the throttle so |
| | 404 | + // token-less login attempts don't count towards the throttle |
| | 405 | + // but wrong-token attempts do. |
| | 406 | + |
| | 407 | + // If the user doesn't have a login token yet, set one. |
| | 408 | + if ( !self::getLoginToken() ) { |
| | 409 | + self::setLoginToken(); |
| | 410 | + return self::NEED_TOKEN; |
| | 411 | + } |
| | 412 | + // If the user didn't pass a login token, tell them we need one |
| | 413 | + if ( !$this->mToken ) { |
| | 414 | + return self::NEED_TOKEN; |
| | 415 | + } |
| | 416 | + |
| 399 | 417 | global $wgPasswordAttemptThrottle; |
| 400 | 418 | |
| 401 | 419 | $throttleCount = 0; |
| — | — | @@ -413,6 +431,11 @@ |
| 414 | 432 | return self::THROTTLED; |
| 415 | 433 | } |
| 416 | 434 | } |
| | 435 | + |
| | 436 | + // Validate the login token |
| | 437 | + if ( $this->mToken !== self::getLoginToken() ) { |
| | 438 | + return self::WRONG_TOKEN; |
| | 439 | + } |
| 417 | 440 | |
| 418 | 441 | // Load $wgUser now, and check to see if we're logging in as the same |
| 419 | 442 | // name. This is necessary because loading $wgUser (say by calling |
| — | — | @@ -575,6 +598,7 @@ |
| 576 | 599 | $wgUser->invalidateCache(); |
| 577 | 600 | } |
| 578 | 601 | $wgUser->setCookies(); |
| | 602 | + self::clearLoginToken(); |
| 579 | 603 | |
| 580 | 604 | // Reset the throttle |
| 581 | 605 | $key = wfMemcKey( 'password-throttle', wfGetIP(), md5( $this->mName ) ); |
| — | — | @@ -593,7 +617,11 @@ |
| 594 | 618 | return $this->cookieRedirectCheck( 'login' ); |
| 595 | 619 | } |
| 596 | 620 | break; |
| 597 | | - |
| | 621 | + |
| | 622 | + case self::NEED_TOKEN: |
| | 623 | + case self::WRONG_TOKEN: |
| | 624 | + $this->mainLoginForm( wfMsg( 'sessionfailure' ) ); |
| | 625 | + break; |
| 598 | 626 | case self::NO_NAME: |
| 599 | 627 | case self::ILLEGAL: |
| 600 | 628 | $this->mainLoginForm( wfMsg( 'noname' ) ); |
| — | — | @@ -937,6 +965,11 @@ |
| 938 | 966 | $template->set( 'canremember', ( $wgCookieExpiration > 0 ) ); |
| 939 | 967 | $template->set( 'remember', $wgUser->getOption( 'rememberpassword' ) or $this->mRemember ); |
| 940 | 968 | |
| | 969 | + if ( !self::getLoginToken() ) { |
| | 970 | + self::setLoginToken(); |
| | 971 | + } |
| | 972 | + $template->set( 'token', self::getLoginToken() ); |
| | 973 | + |
| 941 | 974 | # Prepare language selection links as needed |
| 942 | 975 | if( $wgLoginLanguageSelector ) { |
| 943 | 976 | $template->set( 'languages', $this->makeLanguageSelector() ); |
| — | — | @@ -991,6 +1024,32 @@ |
| 992 | 1025 | global $wgDisableCookieCheck, $wgRequest; |
| 993 | 1026 | return $wgDisableCookieCheck ? true : $wgRequest->checkSessionCookie(); |
| 994 | 1027 | } |
| | 1028 | + |
| | 1029 | + /** |
| | 1030 | + * Get the login token from the current session |
| | 1031 | + */ |
| | 1032 | + public static function getLoginToken() { |
| | 1033 | + global $wgRequest; |
| | 1034 | + return $wgRequest->getSessionData( 'wsLoginToken' ); |
| | 1035 | + } |
| | 1036 | + |
| | 1037 | + /** |
| | 1038 | + * Generate a new login token and attach it to the current session |
| | 1039 | + */ |
| | 1040 | + public static function setLoginToken() { |
| | 1041 | + global $wgRequest; |
| | 1042 | + // Use User::generateToken() instead of $user->editToken() |
| | 1043 | + // because the latter reuses $_SESSION['wsEditToken'] |
| | 1044 | + $wgRequest->setSessionData( 'wsLoginToken', User::generateToken() ); |
| | 1045 | + } |
| | 1046 | + |
| | 1047 | + /** |
| | 1048 | + * Remove any login token attached to the current session |
| | 1049 | + */ |
| | 1050 | + public static function clearLoginToken() { |
| | 1051 | + global $wgRequest; |
| | 1052 | + $wgRequest->setSessionData( 'wsLoginToken', null ); |
| | 1053 | + } |
| 995 | 1054 | |
| 996 | 1055 | /** |
| 997 | 1056 | * @private |
| Index: trunk/phase3/includes/templates/Userlogin.php |
| — | — | @@ -111,6 +111,7 @@ |
| 112 | 112 | </tr> |
| 113 | 113 | </table> |
| 114 | 114 | <?php if( @$this->haveData( 'uselang' ) ) { ?><input type="hidden" name="uselang" value="<?php $this->text( 'uselang' ); ?>" /><?php } ?> |
| | 115 | +<?php if( @$this->haveData( 'token' ) ) { ?><input type="hidden" name="wpLoginToken" value="<?php $this->text( 'token' ); ?>" /><?php } ?> |
| 115 | 116 | </form> |
| 116 | 117 | </div> |
| 117 | 118 | <div id="loginend"><?php $this->msgWiki( 'loginend' ); ?></div> |