# Get the token from DB/cache and clean it up to remove garbage padding.
# This deals with historical problems with bugs and the default column value.
$token = rtrim( $proposedUser->getToken( false ) ); // correct token
- $passwordCorrect = ( strlen( $token ) && $token === $request->getCookie( 'Token' ) );
+ // Make comparison in constant time (bug 61346)
+ $passwordCorrect = strlen( $token ) && $this->compareSecrets( $token, $request->getCookie( 'Token' ) );
$from = 'cookie';
} else {
// No session or persistent login cookie
}
}
+ /**
+ * A comparison of two strings, not vulnerable to timing attacks
+ * @param string $answer the secret string that you are comparing against.
+ * @param string $test compare this string to the $answer.
+ * @return bool True if the strings are the same, false otherwise
+ */
+ protected function compareSecrets( $answer, $test ) {
+ if ( strlen( $answer ) !== strlen( $test ) ) {
+ $passwordCorrect = false;
+ } else {
+ $result = 0;
+ for ( $i = 0; $i < strlen( $answer ); $i++ ) {
+ $result |= ord( $answer{$i} ) ^ ord( $test{$i} );
+ }
+ $passwordCorrect = ( $result == 0 );
+ }
+ return $passwordCorrect;
+ }
+
/**
* Load user and user_group data from the database.
* $this->mId must be set, this is how the user is identified.
return false;
}
- global $wgMemc, $wgRateLimitLog;
+ global $wgMemc;
wfProfileIn( __METHOD__ );
$limits = $wgRateLimits[$action];
// Already pinged?
if ( $count ) {
if ( $count >= $max ) {
- wfDebug( __METHOD__ . ": tripped! $key at $count $summary\n" );
- if ( $wgRateLimitLog ) {
- wfSuppressWarnings();
- file_put_contents( $wgRateLimitLog, wfTimestamp( TS_MW ) . ' ' . wfWikiID() . ': ' . $this->getName() . " tripped $key at $count $summary\n", FILE_APPEND );
- wfRestoreWarnings();
- }
+ wfDebugLog( 'ratelimit', $this->getName() . " tripped! $key at $count $summary");
$triggered = true;
} else {
wfDebug( __METHOD__ . ": ok. $key at $count $summary\n" );
// Don't bother storing default values
$defaultOption = self::getDefaultOption( $key );
if ( ( is_null( $defaultOption ) &&
- !( $value === false || is_null( $value ) ) ) ||
- $value != $defaultOption ) {
+ !( $value === false || is_null( $value ) ) ) ||
+ $value != $defaultOption
+ ) {
$insert_rows[] = array(
- 'up_user' => $userId,
- 'up_property' => $key,
- 'up_value' => $value,
- );
+ 'up_user' => $userId,
+ 'up_property' => $key,
+ 'up_value' => $value,
+ );
}
}
$dbw = wfGetDB( DB_MASTER );
- $hasRows = $dbw->selectField( 'user_properties', '1',
- array( 'up_user' => $userId ), __METHOD__ );
-
- if ( $hasRows ) {
- // Only do this delete if there is something there. A very large portion of
+ // Find and delete any prior preference rows...
+ $res = $dbw->select( 'user_properties',
+ array( 'up_property' ), array( 'up_user' => $userId ), __METHOD__ );
+ $priorKeys = array();
+ foreach ( $res as $row ) {
+ $priorKeys[] = $row->up_property;
+ }
+ if ( count( $priorKeys ) ) {
+ // Do the DELETE by PRIMARY KEY for prior rows. A very large portion of
// calls to this function are for setting 'rememberpassword' for new accounts.
- // Doing this delete for new accounts with no rows in the table rougly causes
- // gap locks on [max user ID,+infinity) which causes high contention since many
- // updates will pile up on each other since they are for higher (newer) user IDs.
- $dbw->delete( 'user_properties', array( 'up_user' => $userId ), __METHOD__ );
+ // Doing a blanket per-user DELETE for new accounts with no rows in the table
+ // causes gap locks on [max user ID,+infinity) which causes high contention since
+ // updates will pile up on each other as they are for higher (newer) user IDs.
+ $dbw->delete( 'user_properties',
+ array( 'up_user' => $userId, 'up_property' => $priorKeys ), __METHOD__ );
}
+ // Insert the new preference rows
$dbw->insert( 'user_properties', $insert_rows, __METHOD__, array( 'IGNORE' ) );
}