Asked  7 Months ago    Answers:  5   Viewed   49 times

My web application uses sessions to store information about the user once they've logged in, and to maintain that information as they travel from page to page within the app. In this specific application, I'm storing the user_id, first_name and last_name of the person.

I'd like to offer a "Keep Me Logged In" option on log in that will put a cookie on the user's machine for two weeks, that will restart their session with the same details when they return to the app.

What is the best approach for doing this? I don't want to store their user_id in the cookie, as it seems like that would make it easy for one user to try and forge the identity of another user.

 Answers

51

OK, let me put this bluntly: if you're putting user data, or anything derived from user data into a cookie for this purpose, you're doing something wrong.

There. I said it. Now we can move on to the actual answer.

What's wrong with hashing user data, you ask? Well, it comes down to exposure surface and security through obscurity.

Imagine for a second that you're an attacker. You see a cryptographic cookie set for the remember-me on your session. It's 32 characters wide. Gee. That may be an MD5...

Let's also imagine for a second that they know the algorithm that you used. For example:

md5(salt+username+ip+salt)

Now, all an attacker needs to do is brute force the "salt" (which isn't really a salt, but more on that later), and he can now generate all the fake tokens he wants with any username for his IP address! But brute-forcing a salt is hard, right? Absolutely. But modern day GPUs are exceedingly good at it. And unless you use sufficient randomness in it (make it large enough), it's going to fall quickly, and with it the keys to your castle.

In short, the only thing protecting you is the salt, which isn't really protecting you as much as you think.

But Wait!

All of that was predicated that the attacker knows the algorithm! If it's secret and confusing, then you're safe, right? WRONG. That line of thinking has a name: Security Through Obscurity, which should NEVER be relied upon.

The Better Way

The better way is to never let a user's information leave the server, except for the id.

When the user logs in, generate a large (128 to 256 bit) random token. Add that to a database table which maps the token to the userid, and then send it to the client in the cookie.

What if the attacker guesses the random token of another user?

Well, let's do some math here. We're generating a 128 bit random token. That means that there are:

possibilities = 2^128
possibilities = 3.4 * 10^38

Now, to show how absurdly large that number is, let's imagine every server on the internet (let's say 50,000,000 today) trying to brute-force that number at a rate of 1,000,000,000 per second each. In reality your servers would melt under such load, but let's play this out.

guesses_per_second = servers * guesses
guesses_per_second = 50,000,000 * 1,000,000,000
guesses_per_second = 50,000,000,000,000,000

So 50 quadrillion guesses per second. That's fast! Right?

time_to_guess = possibilities / guesses_per_second
time_to_guess = 3.4e38 / 50,000,000,000,000,000
time_to_guess = 6,800,000,000,000,000,000,000

So 6.8 sextillion seconds...

Let's try to bring that down to more friendly numbers.

215,626,585,489,599 years

Or even better:

47917 times the age of the universe

Yes, that's 47917 times the age of the universe...

Basically, it's not going to be cracked.

So to sum up:

The better approach that I recommend is to store the cookie with three parts.

function onLogin($user) {
    $token = GenerateRandomToken(); // generate a token, should be 128 - 256 bit
    storeTokenForUser($user, $token);
    $cookie = $user . ':' . $token;
    $mac = hash_hmac('sha256', $cookie, SECRET_KEY);
    $cookie .= ':' . $mac;
    setcookie('rememberme', $cookie);
}

Then, to validate:

function rememberMe() {
    $cookie = isset($_COOKIE['rememberme']) ? $_COOKIE['rememberme'] : '';
    if ($cookie) {
        list ($user, $token, $mac) = explode(':', $cookie);
        if (!hash_equals(hash_hmac('sha256', $user . ':' . $token, SECRET_KEY), $mac)) {
            return false;
        }
        $usertoken = fetchTokenByUserName($user);
        if (hash_equals($usertoken, $token)) {
            logUserIn($user);
        }
    }
}

Note: Do not use the token or combination of user and token to lookup a record in your database. Always be sure to fetch a record based on the user and use a timing-safe comparison function to compare the fetched token afterwards. More about timing attacks.

Now, it's very important that the SECRET_KEY be a cryptographic secret (generated by something like /dev/urandom and/or derived from a high-entropy input). Also, GenerateRandomToken() needs to be a strong random source (mt_rand() is not nearly strong enough. Use a library, such as RandomLib or random_compat, or mcrypt_create_iv() with DEV_URANDOM)...

The hash_equals() is to prevent timing attacks. If you use a PHP version below PHP 5.6 the function hash_equals() is not supported. In this case you can replace hash_equals() with the timingSafeCompare function:

/**
 * A timing safe equals comparison
 *
 * To prevent leaking length information, it is important
 * that user input is always used as the second parameter.
 *
 * @param string $safe The internal (safe) value to be checked
 * @param string $user The user submitted (unsafe) value
 *
 * @return boolean True if the two strings are identical.
 */
function timingSafeCompare($safe, $user) {
    if (function_exists('hash_equals')) {
        return hash_equals($safe, $user); // PHP 5.6
    }
    // Prevent issues if string length is 0
    $safe .= chr(0);
    $user .= chr(0);

    // mbstring.func_overload can make strlen() return invalid numbers
    // when operating on raw binary strings; force an 8bit charset here:
    if (function_exists('mb_strlen')) {
        $safeLen = mb_strlen($safe, '8bit');
        $userLen = mb_strlen($user, '8bit');
    } else {
        $safeLen = strlen($safe);
        $userLen = strlen($user);
    }

    // Set the result to the difference between the lengths
    $result = $safeLen - $userLen;

    // Note that we ALWAYS iterate over the user-supplied length
    // This is to prevent leaking length information
    for ($i = 0; $i < $userLen; $i++) {
        // Using % here is a trick to prevent notices
        // It's safe, since if the lengths are different
        // $result is already non-0
        $result |= (ord($safe[$i % $safeLen]) ^ ord($user[$i]));
    }

    // They are only identical strings if $result is exactly 0...
    return $result === 0;
}
Tuesday, June 1, 2021
 
MoarCodePlz
answered 7 Months ago
74

First: Configure the session.cookie_lifetime directive, either in php.ini, configuration files, or via session_set_cookie_params().

Next, store the username and the hash value of the password in the session, and validate that login on every page. As long as it's still valid, they get to stay logged in.

The session cookie's natural expiration should generally keep things tidy, as you won't have anyone getting logged out in the middle of their session (if the stars aligned for it, of course) if they keep it active. Failing that, though, I'd consider eCartoth's solution a close second, as you could just add a second line to the if statement:

if (my_validate_user_function($_SESSION['username'],$_SESSION['passhash']) 
    && $_SESSION['deathstamp'] > time()
) {
    // user is logged in again, oh boy!
}
else {
    // send in the death robots
    header('Location: /login.php',true,302);
}

EDIT: One thing you might want to consider is session fixation and/or session hijacking. In order to prevent that, I'd recommend one (or both) of two solutions:

  1. store the user's IP address in the session
  2. use session_regenerate_id() after every successful login attempt.
Wednesday, March 31, 2021
 
julesj
answered 9 Months ago
57

Yes. You use cookies to implement the "auto login" (or "remember me") functionality.

This google search or SO search results, should point you to a right direction.

Wednesday, March 31, 2021
 
Asnexplore
answered 9 Months ago
100

What does $conn->query() return, a MySQL resource object like mysql_query() does? If so then it'll always compare "true". mysql_query() only returns FALSE if the query completely fails, like it has a syntax error or a table doesn't exist.

To check if you got any results you need to try to fetch a row from the result set and see if you get anything, via whatever your equivalent of mysql_fetch_row() is.

Important: Your script is vulnerable to SQL injection attacks, or even just odd usernames like o'neil with an apostrophe. You should escape all variables in a query with mysql_real_escape_string() (or equivalent) to make sure your query doesn't get messed up by special characters. Or, even better, use prepared statements which look like

select * from login where username=? and password=sha1(?)

Re: UPDATE

Variables from a form are available via either $_GET or $_POST, depending on which method was used to submit the form. Try if (isset($_POST['remember'])) to see if that check box was checked.

Important: I see that you tried to use a bare $remember to see if the check box was checked. That suggests to me that you are trying to take advantage of the register_globals feature in PHP which makes your GET and POST variables accessible via regular variable names. If that is the case you should heed the warning in the PHP manual!

WARNING

[register_globals] has been DEPRECATED as of PHP 5.3.0 and REMOVED as of PHP 6.0.0. Relying on this feature is highly discouraged.

Use $_GET and $_POST instead. I could tell you how to make if ($remember) work, actually, but given the inherent evil-ness of register_globals I'm not gonna! ;-)

Saturday, May 29, 2021
 
SheppardDigital
answered 7 Months ago
63

Session security risks come from three different possibilities:

  • Prediction
  • Capture
  • Fixation

Prediction would mean that someone that's not the user for whom the session was created guessed their session ID. The chances of that happening are almost 0, although they do grow as more users use the site simultaneously.

With your code, you would make that risk even lower because it would only work if the attacker shared the user agent and the ip of the predicted session. But the difference is trivial in this case.

Fixation would mean that an attacker can create a session and then force another user into using their session. In this case it would depend: If the attacker knows that you are doing it and they fake the user agent and ip of the client, they could fixate the session. Or if they share ip and user agent.

And finally we have session hijacking, probably the most common method of the three. In this case an attacker would somehow gain access to the session id of a valid logged in user, and then use it to log in to their account. As with the previous method, this would only work for them if they know that you are checking the ip and user agent, and faked the same ones as the user. The technique you are using is not unique, and some attackers might fake them just in case.


That being said, is it secure? Yes and no

If you are obsessed with security, the answer is always the same: Use SSL

Unless your code is open source, almost anything you do that changes the behavior of the php sessions will be secure enough.

The only exception to that would be really popular sites that will attract the attention of hackers.

There is some very good documentation on this topic available:

  • http://phpsec.org/projects/guide/4.html
  • PHP Session Security
  • http://www.squarefree.com/securitytips/web-developers.html#CSRF
Wednesday, August 11, 2021
 
VieStar
answered 4 Months ago
Only authorized users can answer the question. Please sign in first, or register a free account.
Not the answer you're looking for? Browse other questions tagged :
 
Share