4
\$\begingroup\$

Old code:

Login system security

This is an update on the code and suggestions I received from generous users on the old thread.

So same rules apply, just check it out, tell me what you think. Is it better or worse? is the security getting better and if so what else can I do to make it better? Are the sessions correctly called/executed? Is the password check/storage correct?

script: ../includes/access.inc.php

<?php
session_start(); //start session
//password hash library
require $_SERVER['DOCUMENT_ROOT'] . '/includes/password_compact/lib/password.php'; 

$GLOBALS["isAdmin"]  = FALSE; //default set not to admin

//set the session time
if (!isset($_SESSION['mintime'])) {
    $_SESSION['mintime'] = array( 0, time() );
}

function userIsLoggedIn()
{
//if the user clicked the logout button
if (isset($_POST['action']) and $_POST['action'] == 'logout') {
    echo "   logout has been clicked.   ";
    unsetSessions(); //return false and clear sessions variables      
}
    //if the user hit the admin view button
if (isset($_POST['action']) and $_POST['action'] == 'admin') {
    header('Location: ' . $_POST['gotoadmin']);
    exit();
}
    //if you are still logged in check the session
if (isset($_SESSION['loggedIn']) and (!empty($_SESSION['user'])) ) {
   return checkSessionID($_SESSION['user']);
}  
    //if post is not set, and if not set action post or the action post is not equal to login
if (!isset($_POST) and (!isset($_POST['action']) or $_POST['action'] != 'login')) {
    return FALSE;
} 
    //if the username post or the password post is empty
if (empty($_POST['user']) or empty($_POST['password'])) {
    return FALSE;
}    

array_push($_SESSION['mintime'], time()); // Push session element onto the end of the time array
array_shift($_SESSION['mintime']); // Shift the session element off the beginning of the time array

// minimum time (in seconds) between valid form submissions    
if ($_SESSION['mintime'][1] - $_SESSION['mintime'][0] < 2) {
    return FALSE;
}

$username = test_input($_POST['user']); //grab the username post and test the input

// are you logging into the admin page or the forum page? cause login.html script is used for both.
if ($GLOBALS['adminPage'] == 1 and $username != 'admin') { 
    return FALSE;
}

$passtemp = test_input($_POST['password']); //grab the password post and test the input

//this session variable determines which PDO connect script to use to log into the database,
// as admin with admin privilages or as standard privilage. 
if ($username == 'admin') {
    $GLOBALS["isAdmin"] = TRUE;
}

if (!databaseContainsUser($username, $passtemp)) { //if you are not in the database/wrong password
    unsetSessions();
    return FALSE;
} //failed the credentials screen   

if (!isset($_SESSION['session_id'])) { //if you have never logged in here before...

    updateSessionID($username); //update your with a query to post a new UUID() in the sessID users column
    setSessionID($username); // select that newly created UUID()) from database and place it into session variable['session_id']

    //set sessions variables (loggedIn, user, pass) the returns true here and exits here once complete.
    setSessions($username, $passtemp);
}    

//if you have been here before but your session variable does not 
//match the one in the database then clear the sessions variables return false     
if ( isset($_SESSION['session_id']) and !checkSessionID($username) ) {
    echo "session_id keys do not match.";
    unsetSessions();
}   

 //If your credentials are good, and you already have a set Session ID and they match up then do this...   
updateSessionID($username); //update the old sessID to a new UUID()
setSessionID($username); //set the new session_id variable to the new one
//setSessions($username, $passtemp);//this could be where the double is coming from

return TRUE;    
} //end function



function databaseContainsUser($username, $passtemp)
{
//I feel as if this could be a potential security issue, how else can i do this?
if ($GLOBALS["isAdmin"] == TRUE) {
    include 'adminconnect.php';
}   

include 'connect.php';

try {
    $sql = "select count(*) from users where username = :user";
    $s   = $pdo->prepare($sql);
    $s->bindValue(':user', $username);
    $s->execute();
}
catch (PDOException $e) {
    echo "Error" . $e->getMessage();
}

$count = $s->fetch();

if (!$count[0] > 0) {
    return FALSE; //failed
} 
if (!isPasswordGood($username, $passtemp)) {
    return FALSE; //failed
}    

return TRUE;
} //end function

function isPasswordGood($username, $passtemp)
{
include 'connect.php';  

//query hash password in database
try {
    $sql = "SELECT password FROM `users` WHERE username = :user";
    $s   = $pdo->prepare($sql);
    $s->bindValue(':user', $username);
    $s->execute();
}
catch (PDOException $e) {
    echo "Error" . $e->getMessage();
} 

$row  = $s->fetch(); //grab hash array
$hash = array_slice($row, 0, 3); //break up the array

if (password_verify($passtemp, $hash[0])) { //call password_verify() on hash
    return TRUE; //password is good
} else {
    return FALSE; //bad
}

} //end isPasswordGood()


//validate input
function test_input($data)
{
    $data = trim($data);
    $data = stripslashes($data);
    $data = htmlspecialchars($data, ENT_QUOTES, 'UTF-8');

    return $data;
}

//set session variables here
function setSessions($username, $passtemp)
{  
$_SESSION['loggedIn'] = TRUE;
$_SESSION['user']     = $username;    
return TRUE;
}

//unset session variables here
function unsetSessions()
{
unset($_SESSION['session_id']);
unset($_SESSION['loggedIn']);
unset($_SESSION['user']);
return FALSE;
}

//this function sets the session ID
function setSessionID($username).........etc......etc.......

script: reguser.html.php

//^other code above/below
function inputData($username, $password, $email)
{
include $_SERVER['DOCUMENT_ROOT'] . '/includes/connect.php';

try {
    $sql = "insert into users (username, password, email, reg_date) values (:username, :password, :email, CURDATE())";
    $s   = $pdo->prepare($sql);
    $s->bindValue(':username', $username);
$s->bindValue(':password', $password);
$s->bindValue(':email', $email);
    $s->execute();
    return TRUE;
}
catch (PDOException $e) {
    echo "Error" . $e->getMessage();
    return FALSE;
}
} //end function
\$\endgroup\$
1
  • \$\begingroup\$ Yes!! So much i can say here! I should finish my dinner before though :D \$\endgroup\$
    – Alex L
    Jun 22 '14 at 1:53
5
\$\begingroup\$

Notes:

  • For the sake of reading it efficiently, I placed your code in my IDE to let it format the indentations.

Here's what looks good

  • Thorough comments. It helps!

  • password_verify($passtemp, $hash[0])! Yes! Beautiful!

  • Tiny other details and stuff that I probably just brushed over but really should be mentioned.


Here's what doesn't look good

  • Indent please.

  • Use of $GLOBALS. There's a time and place for it, but I see it as unnecessary here. Everything in this file is already in the global scope, and anything outside of it should already be able to tell if the user is an admin or not.

  • What is $_SESSION['mintime']? I thought it would come to me, but it hasn't. Is it the minimum time? Either document it better, or come up with a stronger name.

  • The first file is named access.inc.php. At a glance I can tell it does many things besides "accessing". Change the name or break apart your file and require them in a main calling file.

The next few bullet-points will focus on userIsLoggedIn()

  • Typically, function names are verbs, i.e getProperty(), authorizeUser(), logoutUser(). I suggest changing the name to something else.

  • The method is very busy. Lot's of ifs and business going on. Currently it's covering login, logout, authentication, and session handling. To help improve readability, reduce the size of functions.

  • I notice you have a lot of $_POST and $_SESSION values. Things have become hectic with all the checking and returns, ask yourself what you can do to consolidate some of these values.

  • Some lines just aren't making sense....?

Here's one:

if (!isset($_POST) and ( !isset($_POST['action']) or $_POST['action'] != 'login')

Let's break it down:

If nothing was sent to the page, and the action is either not there or is not 'login', return false.

If nothing was sent to the page, then of course action won't be set! Correct me if I've over thought this, but it's essentially a useless check.

How about this one:

if (isset($_SESSION['loggedIn']) and ! empty($_SESSION['user']))

If the user's session says they're logged in, and they have a username in their session, check their session

Basically that's what it's doing. Don't tell me that's not redundant! Moving back to consolidating $_POST and $_SESSION values: how does 'loggedIn' improve your application? It's clutter! Simply, if they have a username, check 'em. If they don't have a username, they're not logged in.

Next up:

if ($username == 'admin') {
    $GLOBALS["isAdmin"] = TRUE;
}

This is currently behind:

databaseContainsUser($username, $passtemp)

Before both of these is $GLOBALS['adminPage']. You didn't mention how this superglobal is set... What if for some reason it's 0. If someone tries to log in with username 'admin', then an attacker could brute-force send passwords to your page easily and possibly gain access to the admin page.

I've rewritten your function below. Mainly re-factoring, because it's hard to tell your intentions so I left something unchanged, because they're up to you to change!

function userIsLoggedIn() {
    $postUsername = $_POST['user'];
    $postPassword = $_POST['password'];
    if (!isset($_POST['action']) or empty($postUsername & $postPassword)) {
        return FALSE;
    }
    switch ($_POST['action']) {
        case 'logout':
            unsetSessions();
            break;
        case 'admin':
            header('Location: ' . $_POST['gotoadmin']);
            exit();
            break;
    }
    if (!empty($_SESSION['user'])) {
        return checkSessionID($_SESSION['user']);
    }
    array_push($_SESSION['mintime'], time());
    array_shift($_SESSION['mintime']);  
    if ($_SESSION['mintime'][1] - $_SESSION['mintime'][0] < 2) {
        return FALSE;
    }
    $username = test_input($postUsername);
    $passtemp = test_input($postPassword);
    if ($GLOBALS['adminPage'] == 1 and $username != 'admin') {
        return FALSE;
    }
    if ($username == 'admin') {
        $GLOBALS["isAdmin"] = TRUE;
    }
    if (!databaseContainsUser($username, $passtemp)) {
        unsetSessions();
        return FALSE;
    }
    if (!isset($_SESSION['session_id'])) {
        setSessions($username);
    } elseif (!checkSessionID($username)) {
        unsetSessions();
    }  
    updateSessionID($username);
    setSessionID($username);
    return TRUE;
}

So it's still pretty long, and since you know the in's and out's of your whole sessions and globals labyrinth, I encourage you to slim that down by at least 10 lines!

Note: empty($postUsername & $postPassword) is experimental. It should work, but I'm not 100% sure it's secure and fool-proof.

End of function!

  • includes are everywhere! Are we just including it, or are we requiring it to be there? You require it!

  • It's good that your catching errors, but echo "Error" . $e->getMessage(); Isn't gunna cut it. It's always a good habit to ensure a clean error. One that has the possibility of exposing your SQL schema is not good.

  • test_input() is a lame name. Even clean() would have been better.

  • I don't see you user the second parameter and the return value of setSessions() anywhere. Remove the second argument and the return statement.

  • unsetSessions() doesn't need a return value either.

End of that file! Next!

  • Next file, reguser.html.php. The name's bad. Is it for a "regular user" or to "register a user"? Spell it out! Why the .html? That's not typical convention and it's hard to classify it as HTML or PHP. If it contains PHP code, it's PHP.

  • inputData(). Input data where? Into the NSA's log files? Give your function names some meat and meaning so we know what's going on here. Especially since it's procedural.

  • include some file. I would think we need an access file to be available. (require it)

  • reg_date for a column is again ambiguous. How's about 'register_date` instead!

  • I can't tell if you're using the return of inputData(), if you're not they shouldn't be there.


Overall, good improvement. You should take your time building these things, I saw a lot of repetition and unnecessary lines of code. The less lines of code you have, the easy the whole thing will be for you! (Not too literally. Don't shrink it all to 1 line)

Feel free to call me out on anything, it's likely I'll edit this with more info or comments.

\$\endgroup\$
1
  • \$\begingroup\$ Wow it looks like I have a lot of work ahead of me, but that's okay cause I have a day off! I will post any questions/comments if i have any here. Once again thank you Alex and I hope to hear back from you. \$\endgroup\$ Jun 22 '14 at 4:40

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.