@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Prevent users from selecting excessively bad passwords based on their username or email address

Summary:
Ref T13216. We occasionally receive HackerOne reports concerned that you can select your username as a password. I suspect very few users actually do this and that this is mostly a compliance/checklist sort of issue, not a real security issue.

On this install, we have about 41,000 user accounts. Of these, 100 have their username as a password (account or VCS). A substantial subset of these are either explicitly intentional ("demo", "bugmenot") or obvious test accounts ("test" in name, or name is a nonsensical string of gibberish, or looks like "tryphab" or similar) or just a bunch of numbers (?), or clearly a "researcher" doing this on purpose (e.g., name includes "pentest" or "xss" or similar).

So I'm not sure real users are actually very inclined to do this, and we can't really ever stop them from picking awful passwords anyway. But we //can// stop researchers from reporting that this is an issue.

Don't allow users to select passwords which contain words in a blocklist: their username, real name, email addresses, or the install's domain name. These words also aren't allowed to contain the password (that is, neither your password nor your username may be a substring of the other one). We also do a little normalization to try to split apart email addresses, domains, and real names, so I can't have "evan1234" as my password.

Test Plan:
- Added unit tests and made them pass.
- Tried to set my password to a bunch of variations of my username / email / domain name / real name / etc, got rejected.

Reviewers: amckinley

Reviewed By: amckinley

Maniphest Tasks: T13216

Differential Revision: https://secure.phabricator.com/D19776

+175 -1
+76
src/applications/auth/__tests__/PhabricatorAuthPasswordTestCase.php
··· 99 99 $this->assertTrue($account_engine->isUniquePassword($password2)); 100 100 } 101 101 102 + public function testPasswordBlocklisting() { 103 + $user = $this->generateNewTestUser(); 104 + 105 + $user 106 + ->setUsername('iasimov') 107 + ->setRealName('Isaac Asimov') 108 + ->save(); 109 + 110 + $test_type = PhabricatorAuthPassword::PASSWORD_TYPE_TEST; 111 + $content_source = $this->newContentSource(); 112 + 113 + $engine = id(new PhabricatorAuthPasswordEngine()) 114 + ->setViewer($user) 115 + ->setContentSource($content_source) 116 + ->setPasswordType($test_type) 117 + ->setObject($user); 118 + 119 + $env = PhabricatorEnv::beginScopedEnv(); 120 + $env->overrideEnvConfig('account.minimum-password-length', 4); 121 + 122 + $passwords = array( 123 + 'a23li432m9mdf' => true, 124 + 125 + // Empty. 126 + '' => false, 127 + 128 + // Password length tests. 129 + 'xh3' => false, 130 + 'xh32' => true, 131 + 132 + // In common password blocklist. 133 + 'password1' => false, 134 + 135 + // Tests for the account identifier blocklist. 136 + 'isaac' => false, 137 + 'iasimov' => false, 138 + 'iasimov1' => false, 139 + 'asimov' => false, 140 + 'iSaAc' => false, 141 + '32IASIMOV' => false, 142 + 'i-am-iasimov-this-is-my-long-strong-password' => false, 143 + 'iasimo' => false, 144 + 145 + // These are okay: although they're visually similar, they aren't mutual 146 + // substrings of any identifier. 147 + 'iasimo1' => true, 148 + 'isa1mov' => true, 149 + ); 150 + 151 + foreach ($passwords as $password => $expect) { 152 + $this->assertBlocklistedPassword($engine, $password, $expect); 153 + } 154 + } 155 + 156 + private function assertBlocklistedPassword( 157 + PhabricatorAuthPasswordEngine $engine, 158 + $raw_password, 159 + $expect_valid) { 160 + 161 + $envelope_1 = new PhutilOpaqueEnvelope($raw_password); 162 + $envelope_2 = new PhutilOpaqueEnvelope($raw_password); 163 + 164 + $caught = null; 165 + try { 166 + $engine->checkNewPassword($envelope_1, $envelope_2); 167 + } catch (PhabricatorAuthPasswordException $exception) { 168 + $caught = $exception; 169 + } 170 + 171 + $this->assertEqual( 172 + $expect_valid, 173 + !($caught instanceof PhabricatorAuthPasswordException), 174 + pht('Validity of password "%s".', $raw_password)); 175 + } 176 + 177 + 102 178 public function testPasswordUpgrade() { 103 179 $weak_hasher = new PhabricatorIteratedMD5PasswordHasher(); 104 180
+63 -1
src/applications/auth/engine/PhabricatorAuthPasswordEngine.php
··· 115 115 // revoked passwords or colliding passwords either, so we can skip these 116 116 // checks. 117 117 118 - if ($this->getObject()->getPHID()) { 118 + $object = $this->getObject(); 119 + 120 + if ($object->getPHID()) { 119 121 if ($this->isRevokedPassword($password)) { 120 122 throw new PhabricatorAuthPasswordException( 121 123 pht( ··· 130 132 'The password you entered is the same as another password '. 131 133 'associated with your account. Each password must be unique.'), 132 134 pht('Not Unique')); 135 + } 136 + } 137 + 138 + // Prevent use of passwords which are similar to any object identifier. 139 + // For example, if your username is "alincoln", your password may not be 140 + // "alincoln", "lincoln", or "alincoln1". 141 + $viewer = $this->getViewer(); 142 + $blocklist = $object->newPasswordBlocklist($viewer, $this); 143 + 144 + // Smallest number of overlapping characters that we'll consider to be 145 + // too similar. 146 + $minimum_similarity = 4; 147 + 148 + // Add the domain name to the blocklist. 149 + $base_uri = PhabricatorEnv::getAnyBaseURI(); 150 + $base_uri = new PhutilURI($base_uri); 151 + $blocklist[] = $base_uri->getDomain(); 152 + 153 + // Generate additional subterms by splitting the raw blocklist on 154 + // characters like "@", " " (space), and "." to break up email addresses, 155 + // readable names, and domain names into components. 156 + $terms_map = array(); 157 + foreach ($blocklist as $term) { 158 + $terms_map[$term] = $term; 159 + foreach (preg_split('/[ @.]/', $term) as $subterm) { 160 + $terms_map[$subterm] = $term; 161 + } 162 + } 163 + 164 + // Skip very short terms: it's okay if your password has the substring 165 + // "com" in it somewhere even if the install is on "mycompany.com". 166 + foreach ($terms_map as $term => $source) { 167 + if (strlen($term) < $minimum_similarity) { 168 + unset($terms_map[$term]); 169 + } 170 + } 171 + 172 + // Normalize terms for comparison. 173 + $normal_map = array(); 174 + foreach ($terms_map as $term => $source) { 175 + $term = phutil_utf8_strtolower($term); 176 + $normal_map[$term] = $source; 177 + } 178 + 179 + // Finally, make sure that none of the terms appear in the password, 180 + // and that the password does not appear in any of the terms. 181 + $normal_password = phutil_utf8_strtolower($raw_password); 182 + if (strlen($normal_password) >= $minimum_similarity) { 183 + foreach ($normal_map as $term => $source) { 184 + if (strpos($term, $normal_password) === false && 185 + strpos($normal_password, $term) === false) { 186 + continue; 187 + } 188 + 189 + throw new PhabricatorAuthPasswordException( 190 + pht( 191 + 'The password you entered is very similar to a nonsecret account '. 192 + 'identifier (like a username or email address). Choose a more '. 193 + 'distinct password.'), 194 + pht('Not Distinct')); 133 195 } 134 196 } 135 197 }
+18
src/applications/auth/password/PhabricatorAuthPasswordHashInterface.php
··· 6 6 PhutilOpaqueEnvelope $envelope, 7 7 PhabricatorAuthPassword $password); 8 8 9 + /** 10 + * Return a list of strings which passwords associated with this object may 11 + * not be similar to. 12 + * 13 + * This method allows you to prevent users from selecting their username 14 + * as their password or picking other passwords which are trivially similar 15 + * to an account or object identifier. 16 + * 17 + * @param PhabricatorUser The user selecting the password. 18 + * @param PhabricatorAuthPasswordEngine The password engine updating a 19 + * password. 20 + * @return list<string> Blocklist of nonsecret identifiers which the password 21 + * should not be similar to. 22 + */ 23 + public function newPasswordBlocklist( 24 + PhabricatorUser $viewer, 25 + PhabricatorAuthPasswordEngine $engine); 26 + 9 27 }
+18
src/applications/people/storage/PhabricatorUser.php
··· 1665 1665 return new PhutilOpaqueEnvelope($digest); 1666 1666 } 1667 1667 1668 + public function newPasswordBlocklist( 1669 + PhabricatorUser $viewer, 1670 + PhabricatorAuthPasswordEngine $engine) { 1671 + 1672 + $list = array(); 1673 + $list[] = $this->getUsername(); 1674 + $list[] = $this->getRealName(); 1675 + 1676 + $emails = id(new PhabricatorUserEmail())->loadAllWhere( 1677 + 'userPHID = %s', 1678 + $this->getPHID()); 1679 + foreach ($emails as $email) { 1680 + $list[] = $email->getAddress(); 1681 + } 1682 + 1683 + return $list; 1684 + } 1685 + 1668 1686 1669 1687 }