38

I have a manager who likes to "deactivate" accounts by replacing the existing bcrypt hash in the database with a simple dash (-).

A dash in a family of bcrypt hashes

This seems to work as the old password is no longer valid and there is no valid bcrypt hash that any other password could match. But I was curious, is this effective or does it create a security risk?

Is this an effective way to make no password work or does it create a larger security risk?

This specific implementation uses the PHP password_verify function but I would like answers to focus on any general implementation. Please do not focus on my manager's bad practice. We also have a boolean field for setting the user to inactive/active which I believe is better and safer but is not what I want answers to focus on.

  • 14
    Hash replacement like this has been the standard practice for a long time in many products. I'm not saying that it is safe or not, just commenting on the practice and the the manager is not crazy for doing so. – schroeder Jun 11 '18 at 8:47
  • 14
    If the user is able to use a "change password" function, he/she could regain access again. – Samuel Jun 11 '18 at 14:23
  • This brings up an interesting followup question - "Is there a hash value for bcrypt that is impossible to create?" to which I suspect the answer is no, but it would be impossible to verify without simultaneously discovering a weakness in bcrypt, which I'm sure many, many brilliant people have attempted to do already. – corsiKa Jun 11 '18 at 20:34
  • If you do this and you later want to reactivate the account it will mean the user will need to reset their password. If you are permanently deactivating accounts then it would be a good idea to remove the old hash. Some people mistakenly reuse passwords or put personal info in them, so it's a risk to them if you hold onto that password hash for longer than you need to. It might be better to instead of "-" use a string the same length as the deleted hash, if it meant it would overwrite it on disk. But DB implementations don't necessarily work this way. Plus the hash still may exist in backups. – Future Security Jun 11 '18 at 22:29
40

In terms of disallowing legitimate login attempts, it's fine. Unless you're using a very weird hash function, there won't be any values which map to -, and it prevents brute force attacks against the missing values if the database is stolen too, which is a positive (they were unlikely, given the use of bcrypt, but this applies even if the implementation is using a terrible method for storing passwords - pretty much anything other than plain text).

In terms of downsides, if the database is taken, it slightly decreases the security of other accounts - the attacker has fewer records to brute force. If they are paying attention, they should probably remove the records which are marked as inactive, but still. I did say "slightly"...

The other risks could be if there are any methods for access which allow bypassing the hash method for comparison (e.g. you have a legacy method which allows supplying the full hash for some reason) - in that case, if you aren't checking the active status carefully, it might allow access by supplying a dash. Ideally, remove this access method, if this is the case.

  • 29
    The last paragraph is especially important: I have personally encountered a system that accepted raw hashes in the login UI for developer convenience (kind of an ad-hoc "impersonate user" function). If that functionality exists, the person adding the - may not even know about it. – Jens Bannmann Jun 11 '18 at 9:56
  • 6
    The last paragraph is irrelevant to the question: If I steal your password file (or /etc/shadow, if appropriate), and there is a broken authentication path that can be called with a hashed password, I can use any one of the valid hashes to get in. – alexis Jun 11 '18 at 10:21
  • 15
    @alexis It's pretty unlikely that you could guess a valid bcrypt hash, but it wouldn't take much to guess a single dash, even if you've not managed to get hold of the database. The last paragraph doesn't require that you've stolen the database. – Matthew Jun 11 '18 at 10:24
  • 4
    @Tom To a point, yes. It's pretty common to try "unexpected" strings in login systems though - it's much more likely that - is in a word list than $2a$04$0SFDjiaxyk26/b4u5NfnHOSJWStdxXE94Wl2YVi4CMhTEiDZYfOWa. The same applies if the replacement string is inactive or disabled, since the question asks about the general principle. – Matthew Jun 11 '18 at 14:01
  • 3
    @Tom From the text in the answer: "remove this access method". I've seen this kind of thing in APIs bolted onto a system without proper thought, where the main route for login is properly hashing, but the API takes a password hash. I don't know why the developers did that, and they were advised not to, but it's not a theoretical idea - there are systems which do that around, despite all the advice to the contrary. – Matthew Jun 11 '18 at 19:11
18

An invalid password hash (usually "*") truly disables password authentication, since there is really no way to supply a value that will hash to it. However, an invalid password hash does not "deactivate" the account if there are other authentication methods, e.g. by ssh private key. In fact, this is a common way of enforcing key-only authentication: an invalid password in combination with an ssh public key in ~user/.ssh. Therefore an invalid hash in the password column should not be interpreted as "account inactive", but "password authentication blocked".

If password access is the only path available, an invalid hash is arguably superior to explicit flagging because it does not need software support: if /etc/password had an activeuser column and I used it while leaving a valid password in the password field, some tools might ignore the activeuser field.

6

Certainly a big difference is that a user being flagged as "inactive" could still log in, except your server checks for the flag and handles the attempt properly. In addition, the flag may be removed, effectively restoring the original state, while changing the hash to "-" cannot be restored without save the password hash somewhere else. Depending on your application, this may or may not be desired behaviour.

A security risk is not created by this approach, as an attacker with access to your database could merely see that this user isn't supposed to be able to log in. As you correctly stated, no bcrypt hash can match "-", as the format alone doesn't match.

As schroeder stated in the comments, this is not a bad practice, but a common concept to prevent users from logging in, without modifying the login-mechanism to take your "inactive" flag into account (inactive users trying to log in are implicitly rejected).

  • 1
    This is an important note: Doing it this way can actually be more secure than an "invalid" or "inactive" flag, as it protects against mistakes. – Tom Jun 11 '18 at 13:56
  • As discussed in the other answers, it does introduce a security risk if there are other ways for a user to login without using their password. – Conor Mancone Jun 11 '18 at 14:58
6

The purpose of secure hashes is to make it difficult to bruteforce the passwords in the database. If it is possible to crack, or even guess, the password from the 'simple hash', then the account passwords are at risk.

But, that's not the whole story. If the purpose is simply to invalidate anyone from being able to use or guess the password from client-side, then it works just fine. This method has been used for a long time. But the password entries on the back end are weakened. This might be just fine if there are other controls, and the risks and impacts of account compromise are low.

  • 3
    How and why are the password entries "weakened"? There's nothing more secure than a door that no key can open. – alexis Jun 12 '18 at 20:10
  • Yeah, those two last sentences sound really vague, with no actual reasons presented. – ilkkachu Jun 13 '18 at 7:20
5

This is another along the same vein as other answers, so let me first reiterate the main point: this can be an effective way to invalidate users as long as there are no other ways in. As an example to watch out for:

How does your system handle logged in users? Presumably once a user logs in your system remembers them until they log out, probably with cookies/sessions. What do you store in the session to make sure your user stays logged in? Most importantly, what happens if a user is logged in and then their password hash is invalidated? If you are storing the password itself in the session they will effectively be logged out immediately, but storing the password in the session would be a bad idea. If you store the password hash in the session then they will also be immediately logged out. If however in the session you simply record the fact that they are logged in and the id of the user they are logged in as, then invalidating the hash will not automatically log out any currently-logged in users. I would consider that a problem, especially if you have long-lived sessions. What if you deactivate someone's hash and a week later they open up their browser and are still logged in anyway? This is worse if the "Update password" function you provide to users doesn't require you to validate their old passwords. In that case someone who is already logged in may stay logged in when you deactivate their hash and then be able to change their password and regain a valid hash, reactivating their account.

Also watch out for API keys. Some systems (Gitlab got some flack for doing this before they changed their systems) have long-lived API keys that are used for API access. A user who knows their API key could continue to use the system even if their password hash is changed.

Given the general concept of defense in depth I would personally vote for having a "formal" deactivation procedure in your system (I normally just have a status column in the user table that is checked during the normal authentication/permission flow). After all, there can be plenty of caveats that would result in a user with a deactivated hash still being able to use the system. Of course, security is always a cost/benefit approach for each entity, so if adding in a formal deactivation flag takes too much time for your system, and none of the "attack" avenues in answers here seem applicable to you, then you are probably fine.

3

Yes, that is the standard way in Unix, and has been for ages.

The shadow(5) manual page answers your question explicitly:

       If the password field contains some string that is not a valid
       result of crypt(3), for instance ! or *, the user will not be
       able to use a unix password to log in (but the user may log in
       the system by other means).

There are additional tricks, e.g. passwd(1) has an option --lock:

   -l, --lock
       Lock the password of the named account. This option disables a
       password by changing it to a value which matches no possible
       encrypted value (it adds a ´!´ at the beginning of the password).

       Note that this does not disable the account. The user may still
       be able to login using another authentication token (e.g. an SSH
       key). To disable the account, administrators should use usermod
       --expiredate 1 (this set the account's expire date to Jan 2,
       1970).

       Users with a locked password are not allowed to change their
       password.
2

The first thing to note is that a "password hash" is not simply a hash of the password, it is a multi-field structure containing not just the hash itself but an indication of what hashing scheme is used and any parameters needed (in the case of bcrypt a salt and a cost parameter)

So the answer to the question of whether an invalid "password hash" results in no working passwords has nothing to do with hash functions, it boils down to how the checking code handles inputs that do not represent a valid password hash in one of the supported formats. One would hope it would consider an invalid hash as non-matching but the documentation isn't clear so we need to read the source.

https://github.com/php/php-src/blob/master/ext/standard/password.c

https://github.com/php/php-src/blob/master/ext/standard/crypt.c

Reading the code for php's password_verify we find it first calls "php_password_determine_algo", if the password starts with $2y$ it is considered to be "bcrypt", if it starts with "$argon2i$" it is considered to be argon2, otherwise it is considered to be "unknown".

If the hash is "bcrypt" or "unknown" it then moves onto calling php_crypt passing it the password and the hash, this attempts to hash the provided password using the method and salt from the existing password, it can do this either by using an internal implementation or by calling the operating system's crypt function. It seems given an invalid existing hash the internal code will fallback to old-school DES, not sure what the system implementation will do.

Coming back to the unknown/bcrypt section in password_verify it seems that it will fail to match if any of the following are true.

  1. php_crypt returns an error.
  2. the length of the hash returned by php_crypt does not match the length of the hash passwed to password_verify
  3. the hash passed to password_verify is less than 13 characters in length
  4. the content of the hash returned by php_crypt does not match the content of the hash passed to password_verify

Your hash of "-" will be rejected by at least rule 3 from this list (likely also rule 1 or 2 but that depends on the behaviour of the crypt implementation used).

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.