0

My Login system doesnt take the usual approach of SELECT * FROM login WHERE user='$user' AND pass='$pass' and counting the rows, rather it pulls the password from the database, and checks against the entered password. I tried the 'or 1=1 trick against it, and it didn't work. So what insecurities do I have in this:

<?php
        $con = mysqli_connect("","root","mypassword","logins"); //Connect to MySql DB, in format host,user,password,db_name
        $query = mysqli_query($con,"SELECT Pass from login WHERE User='" . $_REQUEST["username"] . "'"); //Query that selects the password if the user equals the request string for "username"
        $row = mysqli_fetch_array($query); //Get the result in array form from the query
        if($_REQUEST["password"] == $row[0] && $row[0] != null){ /* Check if Password in the database equals the request string for the password */
                echo 'Login Sucsessful!';
        }
        else{
                echo 'Login failed!';
        }
?>

My other question is why do most people go for the SELECT * FROM login WHERE user='$user' AND pass='$pass' approach, and not this. Also, is there any insecurities with this?

ModerateCarrot
  • 289
  • 1
  • 2
  • 12
  • 3
    You'd get a better response on code review. Plus, your method and other people's are methods that shouldn't be used. Prepared statements are what should be used. – Funk Forty Niner Dec 21 '14 at 16:13
  • 4
    Look into using password_hash and password_verify. This uses an approach similar to yours, but is more secure in that you are not storing plaintext passwords. – Mike Brant Dec 21 '14 at 16:50

4 Answers4

4

Password encryption scheme

About using plaintext passwords, see the other answers. It's not a good choice. BCrypt (readily available with PHP) is the way to go.

To be really paranoid you should make it so that, if the user is not in the database, a fake user is recovered with a surely wrong password (e.g. the password supplied by the browser user, plus one letter). Then run the password check all the same. This will prevent timing attacks designed to fish out what users are in the system (longer check time) and what users are not (fast, immediate fail), by forcing the longer check time for all user regardless of whether they exist or not.

Exploitability

In general, accepting user input without checking/escaping must be considered always insecure.

In your case of course the 1=1 exploit does not work, and the semicolon exploit is harmless against MySQLi.

But what about an injection against User, and supply this in the POST:

username=foo' limit 0 UNION select 'pwn3d!' AS Pass;--
password=pwn3d!

Now the query becomes:

SELECT Pass from login WHERE User='foo' limit 0 UNION select 'pwn3d!' AS Pass;--';

or if one wanted to avoid -- and ; on the ground that some SQL analysis might reject the query as unsound (e.g. because it's a multiple query, which is uncommon or unsupported, and anyway suspicious), this more complicated username

foo' LIMIT 0 UNION SELECT 'pwn3d!' AS Pass UNION SELECT Pass FROM login WHERE ''!='

which will yield:

+--------+
| Pass   |
+--------+
| pwn3d! |
+--------+
1 row in set (0.00 sec)

And of course since we chose the password, this is always equal to the password we supplied.

So the login check will pass.

This is the fiddle (removing SQL comments, which SQLfiddle seemed not to like).

This requires the attacker to know the name of the password field, and in a real world implementation this attack, implemented as above, will not work (the user information is lost). But depending on the real implementation, it is possible to tweak it and actually get logged in as any user, provided you know his/her login name, without even knowing what the password is.

Another interesting possibility would be to store the MD5 hash of the username together with the username. Then the query could be

SELECT * FROM (
    SELECT * FROM login WHERE hashed_username=?
    UNION
    SELECT [list of default fields], ? AS Pass
) LIMIT 1;

which always returns exactly one record whether the user is there or not; the supplied value for hashed_username is md5($_REQUEST['username']) which would prevent any SQL attacks even if we weren't using prepared queries, and with an index on hashed_username guarantees that timing attacks are unfeasible. The supplied value for the default password is BCrypt::hash("_NOT_".$_REQUEST['password']).

At this point we just run BCrypt::verify($record['Pass'], $_REQUEST['password']) and see whether it returns true (user exists, password OK) or false (bad user or bad password). The two execution paths are now close enough that no reasonable amount of bruteforcing can lead an attacker to uncover either a valid username or a password. A suitable BCrypt round value also guarantees that bruteforcing a compromised database isn't really feasible.

At this point the vulnerable spot is password strength. Remember that a password policy so complex that users find practical or necessary to write it down and tape it to the monitor is actually detrimental to security (been there, done that. Sigh).

LSerni
  • 55,617
  • 10
  • 65
  • 107
  • I'd need the implementation details to tell exactly why. My guess is that the user information is lost. Also, while wondering why my original SQLfiddle - the one with -- comments - did not work, I found a couple of pages hinting that some drivers *might* reject queries containing comments, on the grounds that they have no reason of being sent except from exploit code. Which *does* sort of make sense. (I'm wondering whether an exploit is still possible; I might amend my answer later tomorrow). – LSerni Dec 22 '14 at 00:43
1

You should never store passwords as plain text. Doing this may expose all your user's info in case your database is compromised. Since many people use the same password for multiple services, this can be a huge security issue.

You should look into password hashing as suggested in the comments.

You are also vulnerable to SQL injection. Never use information provided by the user directly in your query, as it's possible to write specific string in the username field that may do all kinds of things to your database.

To solve this, look into prepared statements.

Daniel Castro
  • 643
  • 6
  • 16
1

To show how are secure enough (for most cases) login script should look like, I'm taking some parts of classes from this project: php-login

Here is how you would store a password in your database:

// crypt the user's password with the PHP 5.5's password_hash() function, results in a 60 character hash string
// the PASSWORD_DEFAULT constant is defined by the PHP 5.5, or if you are using PHP 5.3/5.4, by the password hashing
// compatibility library. the third parameter looks a little bit shitty, but that's how those PHP 5.5 functions
// want the parameter: as an array with, currently only used with 'cost' => XX.

$user_password_hash = password_hash($user_password, PASSWORD_DEFAULT, array('cost' => $hash_cost_factor));



// write new users data into database using PDO and of course prepared statements

 $query_new_user_insert = $this->db_connection->prepare('INSERT INTO users (user_name, user_password_hash, user_email, user_activation_hash, user_registration_ip, user_registration_datetime) VALUES(:user_name, :user_password_hash, :user_email, :user_activation_hash, :user_registration_ip, now())');
$query_new_user_insert->bindValue(':user_name', $user_name, PDO::PARAM_STR);
$query_new_user_insert->bindValue(':user_password_hash', $user_password_hash, PDO::PARAM_STR);
$query_new_user_insert->bindValue(':user_email', $user_email, PDO::PARAM_STR);
$query_new_user_insert->bindValue(':user_registration_ip', $_SERVER['REMOTE_ADDR'], PDO::PARAM_STR);
$query_new_user_insert->execute();
// id of new user
$user_id = $this->db_connection->lastInsertId();

Notice that we have only inserted a hashed form (password_hash) of the password, not the password itself --> you should never do that! Please note that there are checkups if the username or email is already taken.

You should insert only the hashed value of the password. You should never insert the unhashed password

Now, if we want to login a user, we will use password_verify and again, prepared statements:

$query_user = $this->db_connection->prepare('SELECT * FROM users WHERE user_name = :user_email');
$query_user->bindValue(':user_name', trim($user_name), PDO::PARAM_STR);
$query_user->execute();
// get result row (as an object)
$result_row = $query_user->fetchObject();

// here we will check for the correct password / check if the password hash doesn't fit (mind the ! password_verify):  
if (! password_verify($user_password, $result_row->user_password_hash)) {

// increment the failed login counter for that user
$sth = $this->db_connection->prepare('UPDATE users '
. 'SET user_failed_logins = user_failed_logins+1, user_last_failed_login = :user_last_failed_login '
. 'WHERE user_name = :user_name OR user_email = :user_name');
$sth->execute(array(':user_name' => $user_name, ':user_last_failed_login' => time()));

In this case, we would echo a message or redirect the user who couldn't be identified correctly to the login page again...

Now the part where things match:

    else {
// write user data into PHP SESSION [a file on your server]
$_SESSION['user_id'] = $result_row->user_id;
$_SESSION['user_name'] = $result_row->user_name;
$_SESSION['user_email'] = $result_row->user_email;
$_SESSION['user_logged_in'] = 1;
// declare user id, set the login status to true
$this->user_id = $result_row->user_id;
$this->user_name = $result_row->user_name;
$this->user_email = $result_row->user_email;
$this->user_is_logged_in = true;
// reset the failed login counter for that user
$sth = $this->db_connection->prepare('UPDATE users '
. 'SET user_failed_logins = 0, user_last_failed_login = NULL '
. 'WHERE user_id = :user_id AND user_failed_logins != 0');
$sth->execute(array(':user_id' => $result_row->user_id));

We will set a $_SESSION for the user and identify him/her with that $_SESSION for his stay, you can see this in the library on github. You can also redirect the user now.

You should run a check for logged in / set SESSION on every page load within your login area.

baao
  • 71,625
  • 17
  • 143
  • 203
  • This is a good answer, but I want to clarify one thing for OP. You *should* insert only the hashed value of the password. You *should never* insert the unhashed password. – elixenide Dec 21 '14 at 18:21
  • @EdCottrell that's true, I will add your comment to the answer – baao Dec 21 '14 at 18:22
0

I used to do exactly what you are doing, but I soon came to realise how bad it is in many cases and one of the most least secured ways of communicating with an SQL database.

This website is my go to for connecting to SQL via PHP using PDO connections. I don't remember them off the top of my head so this sits in my bookmarks and has proven to be extremely handy. I use and prefer PDO connections however there are some other methods out there that you can use that achieves a similar function.

Also, in terms of your password use, like Daniel has said storing passwords in plain text is BAD. Use some form of encryption as a MUST. The best is bcrypt which uses the functions password_hash() and password_verify(). It is extremely simple to use and understand how it works. Here's a previously asked StackOverflow question that shows very well how to use bcrypt to hash your passwords.


Long story short:

- Use PDO connections, to prevent from SQL injections
  Tutorial from tutsplus.com

- Never store passwords in plain text. Use some form of hashing to store them.
  Past StackOverflow answer

That's the starter pack you'll need to create secure database habits in no time!

Community
  • 1
  • 1
SteppingHat
  • 1,199
  • 4
  • 19
  • 50