From: Yuri Astrakhan Date: Sun, 27 May 2007 23:50:24 +0000 (+0000) Subject: API: Enabled API login throttling (with amidaniel's help) X-Git-Tag: 1.31.0-rc.0~52791 X-Git-Url: http://git.cyclocoop.org/url?a=commitdiff_plain;h=11522533106f4d28a9efbef8567aac71cc198713;p=lhc%2Fweb%2Fwiklou.git API: Enabled API login throttling (with amidaniel's help) fixed memcached-client comments minor queryRevisions fix --- diff --git a/includes/api/ApiLogin.php b/includes/api/ApiLogin.php index f5cf4042e8..1caac148f7 100644 --- a/includes/api/ApiLogin.php +++ b/includes/api/ApiLogin.php @@ -5,7 +5,8 @@ * * API for MediaWiki 1.8+ * - * Copyright (C) 2006 Yuri Astrakhan @gmail.com + * Copyright (C) 2006-2007 Yuri Astrakhan @gmail.com, + * Daniel Cannon (cannon dot danielc at gmail dot com) * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -29,14 +30,44 @@ if (!defined('MEDIAWIKI')) { } /** + * Unit to authenticate log-in attempts to the current wiki. + * * @addtogroup API */ class ApiLogin extends ApiBase { - + + /** + * The amount of time a user must wait after submitting + * a bad login (will be multiplied by the THROTTLE_FACTOR for each bad attempt) + */ + const THROTTLE_TIME = 10; + + /** + * The factor by which the wait-time in between authentication + * attempts is increased every failed attempt. + */ + const THROTTLE_FACTOR = 1.5; + + /** + * The maximum number of failed logins after which the wait increase stops. + */ + const THOTTLE_MAX_COUNT = 10; + public function __construct($main, $action) { parent :: __construct($main, $action, 'lg'); } + /** + * Executes the log-in attempt using the parameters passed. If + * the log-in succeeeds, it attaches a cookie to the session + * and outputs the user id, username, and session token. If a + * log-in fails, as the result of a bad password, a nonexistant + * user, or any other reason, the host is cached with an expiry + * and no log-in attempts will be accepted until that expiry + * is reached. The expiry is $this->mLoginThrottle. + * + * @access public + */ public function execute() { $name = $password = $domain = null; extract($this->extractRequestParams()); @@ -50,6 +81,15 @@ class ApiLogin extends ApiBase { $result = array (); + $nextLoginIn = $this->getNextLoginTimeout(); + if ($nextLoginIn > 0) { + $result['result'] = 'NeedToWait'; + $result['details'] = "Please wait $nextLoginIn seconds before next log-in attempt"; + $result['wait'] = $nextLoginIn; + $this->getResult()->addValue(null, 'login', $result); + return; + } + $loginForm = new LoginForm($params); switch ($loginForm->authenticateUserData()) { case LoginForm :: SUCCESS : @@ -86,9 +126,89 @@ class ApiLogin extends ApiBase { ApiBase :: dieDebug(__METHOD__, 'Unhandled case value'); } + if ($result['result'] != 'Success') { + $result['wait'] = $this->cacheBadLogin(); + } + // if we were allowed to try to login, memcache is fine + $this->getResult()->addValue(null, 'login', $result); } + + /** + * Caches a bad-login attempt associated with the host and with an + * expiry of $this->mLoginThrottle. These are cached by a key + * separate from that used by the captcha system--as such, logging + * in through the standard interface will get you a legal session + * and cookies to prove it, but will not remove this entry. + * + * Returns the number of seconds until next login attempt will be allowed. + * + * @access private + */ + private function cacheBadLogin() { + global $wgMemc; + + $key = $this->getMemCacheKey(); + $val =& $wgMemc->get( $key ); + + $val['lastReqTime'] = time(); + if (!isset($val['count'])) { + $val['count'] = 1; + } else { + $val['count'] = 1 + $val['count']; + } + + $delay = ApiLogin::calculateDelay($val); + + $wgMemc->delete($key); + $wgMemc->add( $key, $val, $delay ); + + return $delay; + } + + /** + * How much time the client must wait before it will be + * allowed to try to log-in next. + * The return value is 0 if no wait is required. + */ + private function getNextLoginTimeout() { + global $wgMemc; + + $val = $wgMemc->get($this->getMemCacheKey()); + + $elapse = (time() - $val['lastReqTime']) / 1000; // in seconds + $canRetryIn = ApiLogin::calculateDelay($val) - $elapse; + $canRetryIn = $canRetryIn < 0 ? 0 : $canRetryIn; + + return $canRetryIn; + } + + /** + * Based on the number of previously attempted logins, returns + * the delay (in seconds) when the next login attempt will be allowed. + */ + private static function calculateDelay($val) { + // Defensive programming + $count = $val['count']; + $count = $count < 1 ? 1 : $count; + $count = $count > self::THOTTLE_MAX_COUNT ? self::THOTTLE_MAX_COUNT : $count; + + return self::THROTTLE_TIME + self::THROTTLE_TIME * ($count - 1) * self::THROTTLE_FACTOR; + } + + /** + * Internal cache key for badlogin checks. Robbed from the + * ConfirmEdit extension and modified to use a key unique to the + * API login.3 + * + * @return string + * @access private + */ + private function getMemCacheKey() { + return wfMemcKey( 'apilogin', 'badlogin', 'ip', wfGetIP() ); + } + protected function getAllowedParams() { return array ( 'name' => null, @@ -107,7 +227,12 @@ class ApiLogin extends ApiBase { protected function getDescription() { return array ( - 'This module is used to login and get the authentication tokens.' + 'This module is used to login and get the authentication tokens. ' . + 'In the event of a successful log-in, a cookie will be attached ' . + 'to your session. In the event of a failed log-in, you will not ' . + 'be able to attempt another log-in through this method for 60 ' . + 'seconds--this is to prevent its use in aiding automated password ' . + 'crackers.' ); } diff --git a/includes/api/ApiMain.php b/includes/api/ApiMain.php index 54b9f8f697..fa5c6eef32 100644 --- a/includes/api/ApiMain.php +++ b/includes/api/ApiMain.php @@ -52,7 +52,7 @@ class ApiMain extends ApiBase { * List of available modules: action name => module class */ private static $Modules = array ( -// 'login' => 'ApiLogin', // LOGIN is temporarily disabled until it becomes more secure + 'login' => 'ApiLogin', 'query' => 'ApiQuery', 'opensearch' => 'ApiOpenSearch', 'feedwatchlist' => 'ApiFeedWatchlist', diff --git a/includes/api/ApiQueryRevisions.php b/includes/api/ApiQueryRevisions.php index 392295a31e..f070afd649 100644 --- a/includes/api/ApiQueryRevisions.php +++ b/includes/api/ApiQueryRevisions.php @@ -134,7 +134,7 @@ class ApiQueryRevisions extends ApiQueryBase { $this->addWhereFld('rev_page', current(array_keys($pageSet->getGoodTitles()))); if(!is_null($user)) { - $this->addWhere('rev_user_text =' . $this->getDB()->addQuotes($user)); + $this->addWhereFld('rev_user_text', $user); } elseif (!is_null( $excludeuser)) { $this->addWhere('rev_user_text != ' . $this->getDB()->addQuotes($excludeuser)); } diff --git a/includes/memcached-client.php b/includes/memcached-client.php index 1f4bac004d..8e1562ac97 100644 --- a/includes/memcached-client.php +++ b/includes/memcached-client.php @@ -152,7 +152,7 @@ class memcached /** * At how many bytes should we compress? * - * @var interger + * @var integer * @access private */ var $_compress_threshold; @@ -192,7 +192,7 @@ class memcached /** * Total # of bit buckets we have * - * @var interger + * @var integer * @access private */ var $_bucketcount; @@ -200,7 +200,7 @@ class memcached /** * # of total servers we have * - * @var interger + * @var integer * @access private */ var $_active; @@ -272,9 +272,9 @@ class memcached * Adds a key/value to the memcache server if one isn't already set with * that key * - * @param string $key Key to set with data - * @param mixed $val Value to store - * @param interger $exp (optional) Time to expire data at + * @param string $key Key to set with data + * @param mixed $val Value to store + * @param integer $exp (optional) Time to expire data at * * @return boolean * @access public @@ -291,7 +291,7 @@ class memcached * Decriment a value stored on the memcache server * * @param string $key Key to decriment - * @param interger $amt (optional) Amount to decriment + * @param integer $amt (optional) Amount to decriment * * @return mixed FALSE on failure, value on success * @access public @@ -308,7 +308,7 @@ class memcached * Deletes a key from the server, optionally after $time * * @param string $key Key to delete - * @param interger $time (optional) How long to wait before deleting + * @param integer $time (optional) How long to wait before deleting * * @return boolean TRUE on success, FALSE on failure * @access public @@ -506,9 +506,9 @@ class memcached * Increments $key (optionally) by $amt * * @param string $key Key to increment - * @param interger $amt (optional) amount to increment + * @param integer $amt (optional) amount to increment * - * @return interger New key value? + * @return integer New key value? * @access public */ function incr ($key, $amt=1) @@ -524,7 +524,7 @@ class memcached * * @param string $key Key to set value as * @param mixed $value Value to store - * @param interger $exp (optional) Experiation time + * @param integer $exp (optional) Experiation time * * @return boolean * @access public @@ -582,7 +582,7 @@ class memcached * * @param string $key Key to set value as * @param mixed $value Value to set - * @param interger $exp (optional) Experiation time + * @param integer $exp (optional) Experiation time * * @return boolean TRUE on success * @access public @@ -598,7 +598,7 @@ class memcached /** * Sets the compression threshold * - * @param interger $thresh Threshold to compress if larger than + * @param integer $thresh Threshold to compress if larger than * * @access public */ @@ -687,7 +687,7 @@ class memcached /** * Connects $sock to $host, timing out after $timeout * - * @param interger $sock Socket to connect + * @param integer $sock Socket to connect * @param string $host Host:IP to connect to * * @return boolean @@ -807,11 +807,11 @@ class memcached // {{{ _hashfunc() /** - * Creates a hash interger based on the $key + * Creates a hash integer based on the $key * * @param string $key Key to hash * - * @return interger Hash value + * @return integer Hash value * @access private */ function _hashfunc ($key) @@ -830,9 +830,9 @@ class memcached * * @param string $cmd Command to perform * @param string $key Key to perform it on - * @param interger $amt Amount to adjust + * @param integer $amt Amount to adjust * - * @return interger New value of $key + * @return integer New value of $key * @access private */ function _incrdecr ($cmd, $key, $amt=1) @@ -929,7 +929,7 @@ class memcached * @param string $cmd Command to perform * @param string $key Key to act on * @param mixed $val What we need to store - * @param interger $exp When it should expire + * @param integer $exp When it should expire * * @return boolean * @access private