* Add hooks for captcha in main user login form
authorBrion Vibber <brion@users.mediawiki.org>
Mon, 7 May 2007 21:54:06 +0000 (21:54 +0000)
committerBrion Vibber <brion@users.mediawiki.org>
Mon, 7 May 2007 21:54:06 +0000 (21:54 +0000)
* Add hook point for detecting and logging login attempts with invalid password
* Add captcha support for triggering a captcha after a bad password attempt. Legit users shouldn't be inconvenienced much, but password-guesser bots will be severely speedbumped.

ConfirmEdit.i18n.php
ConfirmEdit.php
FancyCaptcha.i18n.php

index 2705c9c4f1286232b72e53f60537440289343385..16a2bf52bd7674a7d2c1437a260b3e9f96ef87ea 100644 (file)
@@ -12,6 +12,8 @@ $wgConfirmEditMessages['en'] = array(
 the box ([[Special:Captcha/help|more info]]):',
        'captcha-addurl' => 'Your edit includes new external links. To help protect against automated
 spam, please solve the simple sum below and enter the answer in the box ([[Special:Captcha/help|more info]]):',
+'captcha-badpass' => 'To help protect against automated password cracking, please solve the simple sum
+below and enter the answer in the box ([[Special:Captcha/help|more info]]):',
        'captcha-createaccount' => 'To help protect against automated account creation, please solve the simple sum
        below and enter the answer in the box ([[Special:Captcha/help|more info]]):',
        'captcha-createaccount-fail' => "Incorrect or missing confirmation code.",
index be229cf3b320b1141370284ac1431c07eb6b3e2e..013ff4c33382e2550c5c7b34b1658ce86d6b464e 100644 (file)
@@ -79,6 +79,7 @@ $wgCaptchaTriggers['edit']          = false; // Would check on every edit
 $wgCaptchaTriggers['create']           = false; // Check on page creation.
 $wgCaptchaTriggers['addurl']        = true;  // Check on edits that add URLs
 $wgCaptchaTriggers['createaccount'] = true;  // Special:Userlogin&type=signup
+$wgCaptchaTriggers['badlogin']      = true;  // Special:Userlogin after failure
 
 /**
  * You may wish to apply special rules for captcha triggering on some namespaces.
@@ -108,7 +109,7 @@ global $wgCaptchaStorageClass;
 $wgCaptchaStorageClass = 'CaptchaSessionStore';
 
 /**
- * Number of sections a captcha session should last in the data cache
+ * Number of seconds a captcha session should last in the data cache
  * before expiring when managing through CaptchaCacheStore class.
  *
  * Default is a half hour.
@@ -116,6 +117,18 @@ $wgCaptchaStorageClass = 'CaptchaSessionStore';
 global $wgCaptchaSessionExpiration;
 $wgCaptchaSessionExpiration = 30 * 60;
 
+/**
+ * Number of seconds after a bad login that a captcha will be shown to
+ * that client on the login form to slow down password-guessing bots.
+ *
+ * Has no effect if 'badlogin' is disabled in $wgCaptchaTriggers or
+ * if there is not a caching engine enabled.
+ *
+ * Default is five minutes.
+ */
+global $wgCaptchaBadLoginExpiration;
+$wgCaptchaBadLoginExpiration = 5 * 60;
+
 /**
  * Allow users who have confirmed their e-mail addresses to post
  * URL links without being harassed by the captcha.
@@ -162,6 +175,10 @@ function ceSetup() {
 
        $wgHooks['UserCreateForm'][] = array( &$wgCaptcha, 'injectUserCreate' );
        $wgHooks['AbortNewAccount'][] = array( &$wgCaptcha, 'confirmUserCreate' );
+       
+       $wgHooks['LoginBadPass'][] = array( &$wgCaptcha, 'triggerUserLogin' );
+       $wgHooks['UserLoginForm'][] = array( &$wgCaptcha, 'injectUserLogin' );
+       $wgHooks['AbortLogin'][] = array( &$wgCaptcha, 'confirmUserLogin' );
 }
 
 /**
@@ -258,6 +275,66 @@ class SimpleCaptcha {
                return true;
        }
 
+       /**
+        * Inject a captcha into the user login form after a failed
+        * password attempt as a speedbump for mass attacks.
+        * @fixme if multiple thingies insert a header, could break
+        * @param SimpleTemplate $template
+        * @return bool true to keep running callbacks
+        */
+       function injectUserLogin( &$template ) {
+               if( $this->isBadLoginTriggered() ) {
+                       global $wgOut;
+                       $template->set( 'header',
+                               "<div class='captcha'>" .
+                               $wgOut->parse( $this->getMessage( 'badlogin' ) ) .
+                               $this->getForm() .
+                               "</div>\n" );
+               }
+               return true;
+       }
+       
+       /**
+        * When a bad login attempt is made, increment an expiring counter
+        * in the memcache cloud. Later checks for this may trigger a
+        * captcha display to prevent too many hits from the same place.
+        * @param User $user
+        * @param string $password
+        * @return bool true to keep running callbacks
+        */
+       function triggerUserLogin( $user, $password ) {
+               global $wgCaptchaTriggers, $wgCaptchaBadLoginExpiration, $wgMemc;
+               if( $wgCaptchaTriggers['badlogin'] ) {
+                       $key = $this->badLoginKey();
+                       $count = $wgMemc->get( $key );
+                       if( !$count ) {
+                               $wgMemc->add( $key, 0, $wgCaptchaBadLoginExpiration );
+                       }
+                       $count = $wgMemc->incr( $key );
+               }
+               return true;
+       }
+       
+       /**
+        * Check if a bad login has already been registered for this
+        * IP address. If so, require a captcha.
+        * @return bool
+        * @access private
+        */
+       function isBadLoginTriggered() {
+               global $wgMemc;
+               return intval( $wgMemc->get( $this->badLoginKey() ) ) > 0;
+       }
+       
+       /**
+        * Internal cache key for badlogin checks.
+        * @return string
+        * @access private
+        */
+       function badLoginKey() {
+               return wfMemcKey( 'captcha', 'badlogin', 'ip', wfGetIP() );
+       }
+       
        /**
         * Check if the submitted form matches the captcha session data provided
         * by the plugin when the form was generated.
@@ -436,6 +513,25 @@ class SimpleCaptcha {
                }
                return true;
        }
+       
+       /**
+        * Hook for user login form submissions.
+        * @param User $u
+        * @param string $message
+        * @return bool true to continue, false to abort user creation
+        */
+       function confirmUserLogin( $u, $pass, &$retval ) {
+               if( $this->isBadLoginTriggered() ) {
+                       $this->trigger = "post-badlogin login '" . $u->getName() . "'";
+                       if( !$this->passCaptcha() ) {
+                               $message = wfMsg( 'captcha-badlogin-fail' );
+                               // Emulate a bad-password return to confuse the shit out of attackers
+                               $retval = LoginForm::WRONG_PASS;
+                               return false;
+                       }
+               }
+               return true;
+       }
 
        /**
         * Given a required captcha run, test form input for correct
index fc876edabeee192b5a2947c78898f7038501b277..14dea05f5b58d3512c42f04c77bf2cf1568fa352 100644 (file)
@@ -13,6 +13,8 @@ function efFancyCaptchaMessages() {
 'en' => array(
 'fancycaptcha-addurl' => 'Your edit includes new external links. To help protect against automated
 spam, please enter the words that appear below in the box ([[Special:Captcha/help|more info]]):',
+'fancycaptcha-badlogin' => 'To help protect against automated password cracking, please enter the words
+that appear below in the box ([[Special:Captcha/help|more info]]):',
 'fancycaptcha-createaccount' => 'To help protect against automated account creation, please enter the words
 that appear below in the box ([[Special:Captcha/help|more info]]):',
 'fancycaptcha-create' => 'To create the page, please enter the words that appear below in the box