21

I'm not a cybersecurity expert but just a webmaster and I wonder if this kind of authentication would be dangerous. I test the password like this on the server side using PHP:

if (isset($_POST['pass_word']) AND $_POST['pass_word'] ==  $passwd)

$passwd comes from my PostgreSQL DB. I thought at least I don't risk SQL injection. Do I risk some other kind of injection? If it's a hazardous way to authenticate please explain why.

nc4pk
  • 105
  • 5
kevin ternet
  • 321
  • 2
  • 6
  • 5
    Regarding SQL injection - it is not possible to say if you are vulnerable or not just from looking at this snippet. – Anders Dec 29 '16 at 15:19
  • 36
    I don't know php, but it seems like you're not hashing passwords. If you're not hashing your authentication seems ok, but your users are at risk. An attacker may obtain access to your database by other meanings and steal user credentials in plaintext – Mr. E Dec 29 '16 at 15:37
  • I might be wrong but you might still be having SQL injection issues. You are just showing how you are comparing password from DB with the password you get from post. While what @Polynomial said about hashing password is true, SQL injection if any will be in the code that actually retrieve the password from the DB (for the particular user) which is not shown in your question. – Nithish Thomas Dec 29 '16 at 22:02
  • 1
    If you have to ask, the answer is probably, "Yes." Unless it's just a standard way of implementing it. If you're a web master and not a dev, try to find a standard module that implements authentication for you. – jpmc26 Dec 29 '16 at 22:29
  • Please don't call network security experts “*cyber* experts”! They're not (usually) related to the field of [cybernetics](//en.wikipedia.org/wiki/Cybernetics). – David Foerster Dec 30 '16 at 08:14
  • 2
    @DavidFoerster Sorry it's because in my country network security experts are called **"experts cybersécurité"** – kevin ternet Dec 30 '16 at 08:19
  • English main stream news call them "cybersecurity experts" as well but I think that's bad practice. There's a joke that anyone who calls herself that is actually more of a conman than a network security expert. – David Foerster Dec 30 '16 at 08:36
  • 1
    There's an established term "web security" or "websec" that refers specifically to web applications. – Kos Dec 31 '16 at 09:10
  • @Kos, this discussion is driving me very well aware of the importance of having "websec" knowledge. And I've been working hardly on this domain for 2 days. I hope I will become stronger in "websec" in 2017. – kevin ternet Dec 31 '16 at 09:32

2 Answers2

67

This looks like you're storing passwords in the clear, which is a bad idea. You should ensure passwords are protected using, at minimum, password_hash() and password_verify(), which use bcrypt under the hood. This is simple, easy, safe, and perfectly acceptable for most scenarios.

Alternatively you can use a slightly more secure method such as Argon2, which won the Password Hashing Competition and is resistant to CPU and GPU cracking, and also aims to minimise the potential for side-channel attacks. There's an article which explains how to use Argon2 as part of libsodium's PHP wrapper, or directly using Paragon's "Halite" library, which offers Argon2 with symmetric encryption on top to prevent database-only access from providing usable hashes, due to the fact that the symmetric key is stored on the server's disk as a file. This option is more complicated, but it does offer some additional security if you're truly paranoid. I'd suggest avoiding this if you're unfamiliar with secure development, though, as the chances of you messing something up are increased.

I'd also recommend using === in order to avoid weird cases of false equality using arrays in URL queries or nulls.

Polynomial
  • 132,208
  • 43
  • 298
  • 379
  • @kevinternet No problem. I wrote a sort of ["history of password hashing"](http://security.stackexchange.com/questions/17421/how-to-store-salt/17435#17435) as part of another answer a while back, which may also be useful to you. – Polynomial Dec 29 '16 at 15:27
  • 26
    `md5('aabg7XSs') == md5('aabC9RqS')` (-> true) is a good example why using `==` is a bad idea. – Arminius Dec 29 '16 at 15:30
  • 20
    @Arminius Good example. For others' benefit, this failure case occurs because the first byte of the resultant hash for each is `0e`, which is considered to be a "float number format string" by PHP, and type coercion causes them to be compared as numbers. See [here](http://stackoverflow.com/questions/12598407/php-expresses-two-different-strings-to-be-the-same) for more details. – Polynomial Dec 29 '16 at 15:38
  • 4
    We probably shouldn't be recommending using *any* sort of equality operator, since that opens you up to timing attacks. – Xiong Chiamiov Dec 29 '16 at 16:54
  • 2
    You may also want to explain why we use password hashing (to protect against leaked databases) or to link to some of the canonical answers on that, salts, etc. – Xiong Chiamiov Dec 29 '16 at 16:57
  • @XiongChiamiov I recommended password_hash and password_verify, which avoids equality operators in this regard. The same goes for Argon2 in the examples given on the linked page. The first link I provided as a comment ("history of password hashing") explains everything you've mentioned and more. Is there anything specific that you feel I've missed? – Polynomial Dec 29 '16 at 18:24
  • About arrays, I also recommand this read : http://security.stackexchange.com/questions/127808/is-array-injection-a-vulnerability-and-what-is-the-proper-term-for-it/131254 – Xavier59 Dec 29 '16 at 19:34
  • @Arminius Except that example uses MD5, which we should be categorically discouraging for password hashing. – jpmc26 Dec 29 '16 at 22:32
  • 1
    @Polynomial The Fractal of Bad Design™ strikes again. – nitro2k01 Dec 30 '16 at 10:10
  • 3
    @jpmc26 The point of the example is that two different strings are apparently equal to php, which was news to me: var_dump('0e087386482136013740957780965295' == '0e041022518165728065344349536299'); outputs true. – SBoss Dec 30 '16 at 15:41
  • 1
    @jpmc26 The use of MD5 was simply to show an example of PHP's horrible type coercion causing an unexpected equality. – Polynomial Dec 30 '16 at 16:05
  • @XiongChiamiov I've never heard of someone exploiting a timing attack when comparing hashes. You'd need to crack the password hash as part of the exploit. – user253751 Dec 31 '16 at 04:04
  • @immibis No, you wouldn't. Assuming you have a list of a few billion common passwords and their corresponding MD5, you can simply try over and over and identify the hashes which produce longer comparison times, iteratively discovering each next byte in the hash and eliminating potential plaintexts. – Polynomial Dec 31 '16 at 11:17
  • @Polynomial ... and iteratively discovering each byte in the hash requires you to crack that prefix of the hash (to find a password that hashes to it), and by the end discovering the last byte requires you to correctly guess the password (or find a hash collision with the password). – user253751 Dec 31 '16 at 11:35
  • @immibis Which is why you need the large dictionary first. The attack only works against dictionary passwords hashed with no salt, but it's roughly O(log n) instead of O(n), which is immensely easier. – Polynomial Dec 31 '16 at 13:19
15

Polynomial's answer is spot on. I wanted to point out other potential vulnerabilities: operator precedence, ease of review, and ease of testing.

Something of the form foo AND bar == baz is vulnerable to getting the precedence wrong. I would scrutinize that in review, there's a long history of security measures being foiled because of compound conditions and precedence.

Now, there's nothing wrong with what you've written, but it's very easy to get it wrong. In most languages it's fine because comparison operators like == are higher precedence than logical operators like AND, so your condition reads as (note the explicit parens):

isset($_POST['pass_word']) AND
  ($_POST['pass_word'] ==  $passwd)

...but it's very easy to get it wrong. For example, AND and && do not have the same precedence. What if instead of using isset you (or someone coming along after you) used a shortcutting logical-or to set a default value? (This is not equivalent to isset, it's just an example of precedence causing security goofs)

$_POST['pass_word'] || '' ==  $passwd

This gotcha line is actually this:

$_POST['pass_word'] || ('' ==  $passwd)

Now anyone with a blank password, perhaps from an error retrieving it, will always login.

When it comes to secure code, make your parens explicit to make your intent known to the reviewer and to the program.


Better yet, avoid compound conditions entirely in secure code. You can't screw up using what you didn't use. For example, this code takes advantage of encapsulation and early return to provide very easy to review and test methods.

// This deals with input and normalizing it.
function can_login($user) {
    if( !isset($_POST['pass_word']) ) {
        // Or it could throw an exception to give the
        // caller more information about why they didn't login
        return false;
    }

    return check_password($user, $_POST['pass_word']);
}

// This only deals with checking a password.
// Don't use this, it still has all the flaws Polynomial
// pointed out.
function check_password($user, $check) {
    // get_password() should throw an exception if it cannot
    // find that user's password to avoid accidentally thinking
    // their password is false or 0 or '' or something. This
    // avoids relying on the caller doing the check for failure.    
    return $check === get_password($user);
}

It might not be the best method, but it is small and easy to test and review. This separates the details of logging a user in (which might be more than a password check, like are they a valid user) from checking their password. Any problems can be spotted and fixed without having to wade through the rest of the login code. A reviewer will check get_password and all the places check_password and can_login are used.

I cannot stress enough how important it is to isolate your secure components into as small a piece as possible, using as simple code as possible, to make them easy to review and test.


BONUS: Here's something I thought of doing, but decided it would make the code less secure.

My original idea was to have one function that returned multiple values: they didn't provide a password, their password was wrong, their password was right. This would allow the caller to know difference between "the password is wrong" and "they didn't provide a password, something is wrong".

function can_login($user) {
    if( !isset($_POST['pass_word']) ) {
        return NO_PASSWORD;
    }

    $password = get_password($user);
    if( $_POST['pass_word'] === $password ) {
        return YES;
    }
    else {
        return NO;
    }
}

And then you call it like so:

$rslt = can_login($user);

if( $rslt == YES ) {
    echo "Login!\n";
}
else if( $rslt == NO ) {
    echo "Wrong password.\n";
}
else if( $rslt == NO_PASSWORD ) {
    echo "No password given.\n";
}

The problem is it makes calling the routine properly more complicated. Worse, if you don't read the docs you'd think it was this:

if( can_login($user) ) {
    echo "Yes!\n";
}
else {
    echo "No!\n";
}

Since can_login always returns a true value, now anyone can login.

It drives home the point: make your secure code as simple and foolproof as possible for the reviewer, for the tester, and for the caller. In this case, exceptions are the right way to go as they provide a channel for getting more information while keeping can_login a simple boolean. If you forget to check the program might crash, but the login fails.

Schwern
  • 1,549
  • 8
  • 17
  • thank's for your response. Indeed I use an object (PHP Class) to load from database the count corresponding to login threw an ORM and then I compare password far from any sql statement. As you emphasis risk of being get wrong, I tried to inject in `$_POST['pass_word']` something like `1' == '1' || 'password`. But that didn't work. So that made me relax a little. – kevin ternet Dec 29 '16 at 20:21