Jump to content

Login with password_hash()


Tom8001

Recommended Posts

Hi, i am trying to make a login with which encrypts passwords using password_hash(), when i use it on the register script and it inserts the hashed password and then goes to check the password when logging in the hash changes.

 

Login Script

<?php

require('/includes/functions.php');
require('/includes/connect.php');

error_reporting(E_ALL | E_NOTICE);

session_start();

if($_SERVER['REQUEST_METHOD'] == "POST")
{
 
    $username = $_POST['username'];
    $password = $_POST['password'];
    
    $username = htmlspecialchars($username, ENT_QUOTES);
    $username = htmlentities($username, ENT_QUOTES);
    
    if(empty($username) || empty($password))
    {
     
        die("You must enter your <b>username</b> and <b>password</b>");
        
    }
    
    $enc_password = password_hash($password, PASSWORD_BCRYPT);
    
    $sql = $handler->prepare("SELECT username, password, rank, active FROM users WHERE username = :username AND password = :password");
    $sql->bindParam(':username', $username, PDO::PARAM_STR, 255);
    $sql->bindParam(':password', $enc_password, PDO::PARAM_STR, 255);
    $sql->execute();
    
    if(password_verify(':password', $enc_password)) {
     
        echo "The passwords match";
        
    } else {
     
        echo "The passwords do not match";
        
    }
    
    if($sql->rowCount())
    {
     
        $row = $sql->fetch();
         
            if($row['active'] == 0)
            {
             
                die("<h3>Your account has been banned.</h3>");
                
            }
            
            if($row['rank'] == 1)
            {
             
                $_SESSION['rank'] = $row['rank'];
                $_SESSION['username'] = $username;
                $_SESSION['loggedIn'] = 1;
                echo '<meta http-equiv="refresh" content="0;admin.php">';
                
            } else if($row['rank'] == 0) {
             
                $_SESSION['rank'] = $row['rank'];
                $_SESSION['username'] = $username;
                $_SESSION['loggedIn'] = 1;
                echo '<meta http-equiv="refresh" content="0;user.php">';
                
            }
        
        $_SESSION['rank'] = 0;
        $_SESSION['username'] = $username;
        $_SESSION['loggedIn'] = 1;
        echo '<meta http-equiv="refresh" content="0;user.php">';
        
    } else {
     
        die("<h3>Login Failed.</h3>");
        
    }
    
}

?>

Register Script

<?php

require('/includes/functions.php');
require('/includes/connect.php');

if($_SERVER['REQUEST_METHOD'] == "POST")
{
    
    $username = $_POST['username'];
    $email = $_POST['email'];
    $password = $_POST['password'];
    $cpassword = $_POST['cpassword'];
    
    $username = htmlspecialchars($username, ENT_QUOTES);
    $username = htmlentities($username, ENT_QUOTES);
    
    if(empty($username) || empty($email) || empty($password) || empty($cpassword))
    {
     
        die("You must enter all fields!");
        
    }
    
    if($cpassword !== $password)
    {
     
        die("Passwords do not match!");
        
    }
    
    if (filter_var($email, FILTER_VALIDATE_EMAIL)) {
    
    } else {
     
        die("You must enter a valid E-Mail Address!");
        
    }
    
     
        $enc_password = password_hash($password, PASSWORD_BCRYPT);
    
    
    $sql = $handler->prepare("INSERT INTO `users` (`username`, `email`, `password`) VALUES (:u, :e, )");
    $sql->bindParam(':u', $username, 255);
    $sql->bindParam(':e', $email, 255);
    $sql->bindParam(':p', $enc_password, 255);
    $sql->execute();
    
    if($sql->rowCount())
    {
     
        echo "Your account has been created!, Redirecting..";
        
        echo "<meta http-equiv='refresh' content='0;login.php'>";
        
    } else {
     
        die("Your account could not be created!");
        
    }
    
    
}

?>

Edited by Tom8001
Link to comment
Share on other sites

Thanks, I'm a bit confused though,

$sql = $handler->prepare("SELECT username, password, rank, active FROM users WHERE username = :username AND password = :p");
    $sql->bindParam(':username', $username, PDO::PARAM_STR, 255);
    $sql->bindParam(':p', $password, PDO::PARAM_STR, 255);
    $sql->execute();
    
    $password = password_verify($password, );

How do i compare it to the database hash?

 

You only use password_hash() when registering. To login, you use the plain-text password and compare it to the hash from the database.

password_verify($plainTextPassword, $hashFromDatabase);
Link to comment
Share on other sites

You cannot check the password using SQL. You need to fetch the password hash from the database and then compare it in PHP using password_verify().

<?php

$user_stmt = $handler->prepare('
  SELECT
    password,
    rank,
    active
  FROM
    users
  WHERE username = :username
');
$user_stmt->execute([
    'username' => $username,
]);
$user_data = $user_stmt->fetch();

if ($user_data)
{
    if (password_verify($password, $user_data['password']))
    {
        echo 'The password is correct';
    }
    else
    {
        echo 'Incorrect password.';
    }
}

  • Like 1
Link to comment
Share on other sites

Note that you're using password_hash() incorrectly or at least not optimally.

 

The point of modern hash algorithms like bcrypt is that you adjust their strength to your specific security requirements and hardware. bcrypt has a single cost parameter for that:

const PASSWORD_HASH_COST = 14;    // put this into a configuration file and adjust it



$example_password = 'Xs761Ic5sAwE9ASSCdqdYB'; 
$hash = password_hash($example_password, PASSWORD_BCRYPT, ['cost' => PASSWORD_HASH_COST]);

The higher the cost parameter, the harder it is for an attacker to perform a brute-force attack against the password. But of course a high cost factor also slows down the log-in procedure and stresses your CPU.

 

Finding a good balance between security and usability is very important when you use bcrypt. A common recommendation is that you decide how long the hashing may take. For example, 1 second for a standard account and 3 seconds for an admin-like account should be acceptable. Then you hash a test password with different cost parameters until you've reached the chosen time. That's the right value. Of course you'll have to update the parameter when you move the application to a different server, so it's a good idea to put it into a configuration file.

 

Secondly, bcrypt has two nasty pitfalls:

  • Using passwords longer than 56 bytes violates the specification, and passwords longer than 72 bytes are silently truncated. This can become a real problem if you're using a multibyte character encoding like UTF-8.
  • Any nullbyte within the password will cut off the password at that point.

There are two solutions. Either you validate the password and reject it if it has any of the above problems. Or you run the password through a hash algorithm like SHA-256 prior to using bcrypt:

<?php

const PASSWORD_HASH_COST = 14;



// test data
$password = 'Xs761Ic5sAwE9ASSCdqdYB';

// hash the password with SHA-256 and encode the raw output with Base64
$raw_intermediate_hash = hash('sha256', $password, true);
$encoded_intermediate_hash = base64_encode($raw_intermediate_hash);

// now hash the encoded SHA-256 hash with bcrypt
$final_hash = password_hash($encoded_intermediate_hash, PASSWORD_BCRYPT, ['cost' => PASSWORD_HASH_COST]);

var_dump($final_hash);

This automatically solves both problems: The password is “compressed” to a fixed length of 256 bits, and then those bits are encoded with Base64, yielding exactly 44 Base64 characters. So the bcrypt input cannot become too long, and it cannot cointain nullbytes, regardless of the original password.

  • Like 2
Link to comment
Share on other sites

I changed my code to this

<?php

require('/includes/functions.php');
require('/includes/connect.php');

error_reporting(E_ALL | E_NOTICE);

session_start();

if($_SERVER['REQUEST_METHOD'] == "POST")
{
 
    $username = $_POST['username'];
    $password = $_POST['password'];
    
    $username = htmlspecialchars($username, ENT_QUOTES);
    $username = htmlentities($username, ENT_QUOTES);
    
    if(empty($username) || empty($password))
    {
     
        die("You must enter your <b>username</b> and <b>password</b>");
        
    }
$user_stmt = $handler->prepare('
  SELECT
    password,
    rank,
    active
  FROM
    users
  WHERE username = :username
');
$user_stmt->execute([
    'username' => $username,
    'password' => $password,
]); //This is line 37
    
$user_data = $user_stmt->fetch();

if ($user_data)
{
    if (password_verify($password, $user_data['password']))
    {
        echo 'The password is correct';
    }
    else
    {
        echo 'Incorrect password.';
    }
}
    
    if($$user_stmt->rowCount())
    {
     
        $row = $sql->fetch();
         
            if($row['active'] == 0)
            {
             
                die("<h3>Your account has been banned.</h3>");
                
            }
            
            if($row['rank'] == 1)
            {
             
                $_SESSION['rank'] = $row['rank'];
                $_SESSION['username'] = $username;
                $_SESSION['loggedIn'] = 1;
                echo '<meta http-equiv="refresh" content="0;admin.php">';
                
            } else if($row['rank'] == 0) {
             
                $_SESSION['rank'] = $row['rank'];
                $_SESSION['username'] = $username;
                $_SESSION['loggedIn'] = 1;
                echo '<meta http-equiv="refresh" content="0;user.php">';
                
            }
        
        $_SESSION['rank'] = 0;
        $_SESSION['username'] = $username;
        $_SESSION['loggedIn'] = 1;
        echo '<meta http-equiv="refresh" content="0;user.php">';
        
    } else {
     
        die("<h3>Login Failed.</h3>");
        
    }
    
}

?>

I am getting this error Fatal error: Uncaught exception 'PDOException' with message 'SQLSTATE[HY093]: Invalid parameter number: number of bound variables does not match number of tokens' on line 37

Link to comment
Share on other sites

 

You cannot check the password using SQL. You need to fetch the password hash from the database and then compare it in PHP using password_verify().

<?php

$user_stmt = $handler->prepare('
  SELECT
    password,
    rank,
    active
  FROM
    users
  WHERE username = :username
');
$user_stmt->execute([
    'username' => $username,
]);
$user_data = $user_stmt->fetch();

if ($user_data)
{
    if (password_verify($password, $user_data['password']))
    {
        echo 'The password is correct';
    }
    else
    {
        echo 'Incorrect password.';
    }
}

 

 

@Jacques1,

 

Wanted your input on the username/password selection comparison.

 

From old school Mysql days I had learned to only WHERE the username, not WHERE username= AND password= and then do the password check after just like you did here so you weren't throwing more user supplied data at the database or some security related issue. Dont remember the details as to why now.

 

With PDO and prepared statements does it even matter which way you do it? What do you say about the two options?

Link to comment
Share on other sites

All modern password hash algorithms use salting, so it's impossible to recalculate the hash from the user-supplied password alone. bcrypt actually has three input parameters:

  • the password
  • the cost factor
  • the 128-bit salt

The cost factor and salt are encoded within the bcrypt hash. To verify a password, you need to load the entire hash into PHP and use password_verify(). This function extracts the original parameters, hashes the password with those parameters and compares the resulting hash with the original hash.

 

Some database systems like PostgreSQL have bcrypt built in, so you theoretically mimic the old-school queries by providing the username and the plaintext password:

SELECT
   user_id
FROM
  users
WHERE
  username = :username
  AND crypt(:password, password_hash) = password_hash
;

However, this is not advisable, because sharing the password with the database system increases the risk of leaking it. For example, the password may appear in the query log, or it may be caught by an eavesdropper in case of a remote database.

Link to comment
Share on other sites

the password may appear in the query log, or it may be caught by an eavesdropper in case of a remote database.

 

I could have sworn there was something else but I cant remember what it was. I have always just did WHERE username= only for the last umteen years.

 

Once I learned the "right" way to do something there was no reason to remember why it was right after all these years. Now its bugging me not remembering. The only thing I remember was it was way back when it was commonplace to put plaintext passwords in the db before md5 passwords started catching on.

Edited by benanamen
Link to comment
Share on other sites

Apart from the problems mentioned above, there's nothing inherently wrong with including the password hash in the WHERE clause.

 

The only reason I could think of is that one might want to distinguish between a nonexistent account and a wrong password. Or maybe it was considered some kind of micro-optimization.

Link to comment
Share on other sites

Ok, I finally remembered why you needed to only WHERE the username. After digging through my ancient archives I found a script that will demonstrate. The issue was SQL Injection and being able to login without a username and password. Security problem right? Just put the provided Injection examples in the username and password fields and the Injection Attack will give you the username and password, or in an old real world example would have logged you in.

/*
Source Database       : sqlinjection
*/

-- ----------------------------
-- Table structure for users
-- ----------------------------
DROP TABLE IF EXISTS `users`;
CREATE TABLE `users` (
  `user_id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(255) DEFAULT NULL,
  `password` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of users
-- ----------------------------
INSERT INTO `users` VALUES ('1', 'username', 'password');
<!DOCTYPE html>

<html>

<head>
  <title></title>
</head>

<body>
This works:<br>
anything' OR 'x'='x<br>
' or '1'='1<br>
'OR''='<br>
<form action="<? echo $_SERVER['PHP_SELF'];?>" method="POST">
Username:<input name="username" type="text"><br>
Password:<input name="password" type="text">
<input type="submit" name="Submit" value="Submit">
</form>
<?php
if ($_POST)
    {
    $DBhost     = "localhost";
    $DBusername = "root";
    $DBpassword = "";
    $DBname     = "sqlinjection";
    $DBtable    = "users";

    $con = @mysql_connect($DBhost, $DBusername, $DBpassword);
    mysql_select_db("$DBname");

    $sql    = "SELECT * FROM users WHERE username = '{$_POST['username']}' AND password='{$_POST['password']}' ";
    $result = mysql_query($sql);
    $row    = mysql_fetch_array($result);
    echo "<p>$sql</p>";
    echo "{$row['username']} {$row['password']}";
    }
?>
</body>
</html> 
Edited by benanamen
Link to comment
Share on other sites

Here's something that might help you out or get you going in the right direction - I can't guarantee that it'll work for the might be bugs (errors) that I have overlooked. ;D

<?php

require('/includes/functions.php');
require('/includes/connect.php');

error_reporting(E_ALL | E_NOTICE);

session_start(); // I would put this in the connect.php or a configuration file that goes at on every page (Best Option):

if ($_SERVER['REQUEST_METHOD'] == "POST") {
    $username = filter_input(INPUT_POST, 'username', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
    $password = filter_input(INPUT_POST, 'password', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
    if (empty(trim($username)) || empty(trim($password))) {
        die("You must enter your <b>username</b> and <b>password</b>");
    }
    /* Setup the Query for reading in login data from database table */
    $query= 'SELECT username, password, rank, active FROM users WHERE username=:username';
    
    try {
        $stmt = $handler->prepare($query); // Prepare the query:
        $stmt->execute([':username' => $data['username']]); // Execute the query with the supplied user's parameter(s):
    } catch (Exception $ex) {
        die("Failed to run query: " . $ex->getMessage()); // Do Not Use in Production Website - Log error or email error to admin:
    }

    $stmt->setFetchMode(PDO::FETCH_OBJ); // Fetch data as object(s):
    $user = $stmt->fetch(); // Fetch the data:

    /* If username is in database table then it is TRUE */
    if ($user) {
        $loginStatus = password_verify($password, $user->password); // Check the user's entry to the stored password:
        unset($password, $user->password); // Password(s) not needed then unset the password(s)!:
    } else {
        return FALSE; // Return if no user is found in database table:
    }

    /*
     * If passwords matches and user is active then set user's account into sessions
     * then in a configuration file of some sore you can do something like
     * $user = isset($_SESSION['user']) ? $_SESSION['user'] : NULL;
     * that way all you have to do to access a user who is logged in is 
     * $user->username for example (accessing the object(s))
     */
    if ($loginStatus && $user->active === 1) {
        $_SESSION['user'] = $user; // Set the session variable of user:
        return TRUE; // Everything is OK (Passwords match && user is active):
    } else {
        return FALSE; // Invalid password was entered:
    }
   
}
Edited by Strider64
Link to comment
Share on other sites

Here's something that might help you out or get you going in the right direction - I can't guarantee that it'll work for the might be bugs (errors) that I have overlooked. ;D

You don't need to filter HTML special characters when interacting with a database.

 

$stmt->execute([':username' => $data['username']]);
Where did $data['username'] come from?
Link to comment
Share on other sites

  • 2 weeks later...

What does that have to do with including a WHERE clause for the password? You'd have SQL injection either way.

 

Yes, but with the username AND password hacked you are logged in. You are not going to be able to login with just the username only. Test it and see. 

 

Dont forget, that was from way back in olden days like php3

Edited by benanamen
Link to comment
Share on other sites

You could combine an arbitrary username with a known password by using a UNION:

SELECT username, password FROM users WHERE username = '
-- begin injection
' UNION SELECT 'admin', password FROM users WHERE username = 'my_own_username
-- end injection
'

So your own password would match for a different user.

 

But all of those examples really trivialize the threat of SQL injections. This isn't about logging in as somebody else and doing some shenanigans. A single injection vulnerability is enough to download the entire database and maybe even take over the server (since MySQL has the ability to write output files, a poorly administered server will allow you to place arbitrary scripts in the filesystem).

 

In that regard, it doesn't matter how the vulnerable query looks like or how many input values it takes. Any injection vulnerabiltiy can potentially compromise the entire server.

Link to comment
Share on other sites

This thread is more than a year old. Please don't revive it unless you have something important to add.

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...

Important Information

We have placed cookies on your device to help make this website better. You can adjust your cookie settings, otherwise we'll assume you're okay to continue.