From: Brian Wolff Date: Sun, 22 Nov 2015 07:45:02 +0000 (-0500) Subject: Add support for blacklisting common passwords X-Git-Tag: 1.31.0-rc.0~8808^2 X-Git-Url: http://git.cyclocoop.org/%24action?a=commitdiff_plain;h=2d15dcfc3f4bf81552b378ce5003661c1681b38c;p=lhc%2Fweb%2Fwiklou.git Add support for blacklisting common passwords This changes the default config to not allow the top 25 passwords to be used by Sysop/Crats. This should almost certainly be set to a higher number, but I think its best to wait until after this is comitted to argue over what the best value is. I would expect that once this is comitted, there would be a config change for wmf wikis, so that there is no change until this has been discussed with the community. The included common password file was generated from the first 10000 entries of https://github.com/danielmiessler/SecLists/blob/master/Passwords/rockyou.txt?raw=true 10,000 was chosen based on csteipp's suggestion. Change-Id: I26a9e8f2318a1eed33d7638b125695e8de3a9796 --- diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index bf6e245d72..be6b5ecb4e 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -4352,6 +4352,10 @@ $wgActiveUserDays = 30; * - PasswordCannotMatchUsername - Password cannot match username to * - PasswordCannotMatchBlacklist - Username/password combination cannot * match a specific, hardcoded blacklist. + * - PasswordCannotBePopular - Blacklist passwords which are known to be + * commonly chosen. Set to integer n to ban the top n passwords. + * If you want to ban all common passwords on file, use the + * PHP_INT_MAX constant. * @since 1.26 */ $wgPasswordPolicy = array( @@ -4360,11 +4364,13 @@ $wgPasswordPolicy = array( 'MinimalPasswordLength' => 8, 'MinimumPasswordLengthToLogin' => 1, 'PasswordCannotMatchUsername' => true, + 'PasswordCannotBePopular' => 25, ), 'sysop' => array( 'MinimalPasswordLength' => 8, 'MinimumPasswordLengthToLogin' => 1, 'PasswordCannotMatchUsername' => true, + 'PasswordCannotBePopular' => 25, ), 'bot' => array( 'MinimalPasswordLength' => 8, @@ -4384,6 +4390,7 @@ $wgPasswordPolicy = array( 'PasswordCannotMatchUsername' => 'PasswordPolicyChecks::checkPasswordCannotMatchUsername', 'PasswordCannotMatchBlacklist' => 'PasswordPolicyChecks::checkPasswordCannotMatchBlacklist', 'MaximalPasswordLength' => 'PasswordPolicyChecks::checkMaximalPasswordLength', + 'PasswordCannotBePopular' => 'PasswordPolicyChecks::checkPopularPasswordBlacklist' ), ); @@ -7776,6 +7783,19 @@ $wgVirtualRestConfig = array( */ $wgSearchRunSuggestedQuery = true; +/** + * Where popular password file is located. + * + * Default in core contains 50,000 most popular. This config + * allows you to change which file, in case you want to generate + * a password file with > 50000 entries in it. + * + * @see maintenance/createCommonPasswordCdb.php + * @since 1.27 + * @var string path to file + */ +$wgPopularPasswordFile = __DIR__ . '/../serialized/commonpasswords.cdb'; + /** * For really cool vim folding this needs to be at the end: * vim: foldmarker=@{,@} foldmethod=marker diff --git a/includes/password/PasswordPolicyChecks.php b/includes/password/PasswordPolicyChecks.php index eb4a9582dc..b1098f5b84 100644 --- a/includes/password/PasswordPolicyChecks.php +++ b/includes/password/PasswordPolicyChecks.php @@ -20,6 +20,8 @@ * @file */ +use \Cdb\Reader as CdbReader; + /** * Functions to check passwords against a policy requirement * @since 1.26 @@ -112,4 +114,50 @@ class PasswordPolicyChecks { return $status; } + /** + * Ensure that password isn't in top X most popular passwords + * + * @param $policyVal int Cut off to use. Will automatically shrink to the max + * supported for error messages if set to more than max number of passwords on file, + * so you can use the PHP_INT_MAX constant here safely. + * @param $user User + * @param $password String + * @since 1.27 + * @return Status + */ + public static function checkPopularPasswordBlacklist( $policyVal, User $user, $password ) { + global $wgPopularPasswordFile, $wgSitename; + $status = Status::newGood(); + if ( $policyVal > 0 ) { + $langEn = Language::factory( 'en' ); + $passwordKey = $langEn->lc( trim( $password ) ); + + // People often use the name of the current site, which won't be + // in the common password file. Also check '' for people who use + // just whitespace. + $sitename = $langEn->lc( trim( $wgSitename ) ); + $hardcodedCommonPasswords = array( '', 'wiki', 'mediawiki', $sitename ); + if ( in_array( $passwordKey, $hardcodedCommonPasswords ) ) { + $status->error( 'passwordtoopopular' ); + return $status; + } + + // This could throw an exception, but there's not a good way + // of failing gracefully, if say the file is missing, so just + // let the exception fall through. + // Format of cdb file is mapping password => popularity rank. + // See maintenance/createCommonPasswordCdb.php + $db = CdbReader::open( $wgPopularPasswordFile ); + + $res = $db->get( $passwordKey ); + if ( $res && (int)$res <= $policyVal ) { + // Note: If you want to find the true number of common + // passwords stored (for reporting the error), you have to take + // the max of the policyVal and $db->get( '_TOTALENTRIES' ). + $status->error( 'passwordtoopopular' ); + } + } + return $status; + } + } diff --git a/languages/i18n/en.json b/languages/i18n/en.json index acc50f1753..c9e77c597f 100644 --- a/languages/i18n/en.json +++ b/languages/i18n/en.json @@ -470,6 +470,7 @@ "wrongpasswordempty": "Password entered was blank.\nPlease try again.", "passwordtooshort": "Passwords must be at least {{PLURAL:$1|1 character|$1 characters}}.", "passwordtoolong": "Passwords cannot be longer than {{PLURAL:$1|1 character|$1 characters}}.", + "passwordtoopopular": "Commonly chosen passwords cannot be used. Please choose a more unique password.", "password-name-match": "Your password must be different from your username.", "password-login-forbidden": "The use of this username and password has been forbidden.", "mailmypassword": "Reset password", diff --git a/languages/i18n/qqq.json b/languages/i18n/qqq.json index e0dd3f4693..173e825362 100644 --- a/languages/i18n/qqq.json +++ b/languages/i18n/qqq.json @@ -644,6 +644,7 @@ "wrongpasswordempty": "Error message displayed when entering a blank password.\n{{Identical|Please try again}}", "passwordtooshort": "This message is shown in [[Special:Preferences]] and [[Special:CreateAccount]].\n\nParameters:\n* $1 - the minimum number of characters in the password", "passwordtoolong": "This message is shown in [[Special:Preferences]], [[Special:CreateAccount]], and [[Special:Userlogin]].\n\nParameters:\n* $1 - the maximum number of characters in the password", + "passwordtoopopular": "Shown if the user chooses a really popular password.", "password-name-match": "Used as error message when password validity check failed.", "password-login-forbidden": "Error message shown when the user has tried to log in using one of the special username/password combinations used for MediaWiki testing. (See [[mwr:75589]], [[mwr:75605]].)", "mailmypassword": "Used as label for Submit button in [[Special:PasswordReset]].\n{{Identical|Reset password}}", diff --git a/maintenance/createCommonPasswordCdb.php b/maintenance/createCommonPasswordCdb.php new file mode 100644 index 0000000000..c6787122b0 --- /dev/null +++ b/maintenance/createCommonPasswordCdb.php @@ -0,0 +1,117 @@ +mDescription = "Generate CDB file of common passwords"; + $this->addOption( 'limit', "Max number of passwords to write", false, true, 'l' ); + $this->addArg( 'inputfile', 'List of passwords (one per line) to use or - for stdin', true ); + $this->addArg( + 'output', + "Location to write CDB file to (Try $IP/serialized/commonpasswords.cdb)", + true + ); + } + + public function execute() { + $limit = (int)$this->getOption( 'limit', PHP_INT_MAX ); + $langEn = Language::factory( 'en' ); + + $infile = $this->getArg( 0 ); + if ( $infile === '-' ) { + $infile = 'php://stdin'; + } + $outfile = $this->getArg( 1 ); + + if ( !is_readable( $infile ) && $infile !== 'php://stdin' ) { + $this->error( "Cannot open input file $infile for reading", 1 ); + } + + $file = fopen( $infile, 'r' ); + if ( $file === false ) { + $this->error( "Cannot read input file $infile", 1 ); + } + + try { + $db = \Cdb\Writer::open( $outfile ); + + $alreadyWritten = array(); + $skipped = 0; + for ( $i = 0; ( $i - $skipped ) < $limit; $i++ ) { + if ( feof( $file ) ) { + break; + } + $rawLine = fgets( $file ); + + if ( $rawLine === false ) { + $this->error( "Error reading input file" ); + break; + } + if ( substr( $rawLine, -1 ) !== "\n" && !feof( $file ) ) { + // We're assuming that this just won't happen. + $this->error( "fgets did not return whole line at $i??" ); + } + $line = $langEn->lc( trim( $rawLine ) ); + if ( $line === '' ) { + $this->error( "Line number " . ( $i + 1 ) . " is blank?" ); + $skipped++; + continue; + } + if ( isset( $alreadyWritten[$line] ) ) { + $this->output( "Password '$line' already written (line " . ( $i + 1 ) .")\n" ); + $skipped++; + continue; + } + $alreadyWritten[$line] = true; + $db->set( $line, $i + 1 - $skipped ); + } + // All caps, so cannot conflict with potential password + $db->set( '_TOTALENTRIES', $i - $skipped ); + $db->close(); + + $this->output( "Successfully wrote " . ( $i - $skipped ) . + " (out of $i) passwords to $outfile\n" + ); + } catch ( \Cdb\Exception $e ) { + $this->error( "Error writing cdb file: " . $e->getMessage(), 2 ); + } + + } +} + +$maintClass = "GenerateCommonPassword"; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/serialized/commonpasswords.cdb b/serialized/commonpasswords.cdb new file mode 100644 index 0000000000..7b7b043171 Binary files /dev/null and b/serialized/commonpasswords.cdb differ