#1
  1. --
    Devshed Expert (3500 - 3999 posts)

    Join Date
    Jul 2012
    Posts
    3,923
    Rep Power
    1045

    Making PHP sessions secure


    When it comes to sessions, many people rely on PHP to take care of the details. This is a very bad idea. The standard configuration has plenty of weaknesses and is far from being secure. Fortunately, you can make it secure in a few steps.

    I've attached a test script you can use for checking the PHP configuration. If you run it on a live webserver, you should of course protect the folder with a password so that it's not accessible for everybody.



    Strong session IDs

    Standard session IDs generated by PHP are not random. They’re derived from certain data like the client IP address, the server time and the state of a simple random number generator. This makes them predictable under certain circumstances. And once an attacker can predict the session IDs, it's usually trivial to hijack the session.

    The solution is to use actual random data from the operating system to generate the IDs. This can be done by setting two configuration directives: session.entropy_file and session.entropy_length. The first value specifies the random number generator to read from. It should be something like /dev/urandom. And the second value is the number of bytes to read. Set it to at least 16.

    It might also be a good idea to change the hash algorithm which is used for generating the ID. By default, PHP uses the obsolete MD5 algorithm. Use SHA-2 instead.

    Code:
    session.entropy_file		/dev/urandom
    session.entropy_length		16
    session.hash_function		sha256


    Cookies only

    Exchanging the session ID through the URL is a major security risk. It allows an attacker to perform a session fixation : The attacker starts a session and then passes a URL with their own session ID to a victim. If the victim uses this URL and logs in, the attacker's session will contain all user data (unless the session ID has been regenerated; see below). The session ID will also be leaked in all kinds of ways: inexperienced users may share a URL with their ID, the referrer and the server log will contain the ID etc.

    That's why it's very important to only use cookies for exchanging the session ID. Note that's it's not enough to simply to put the ID into URLs. You must actively tell PHP to reject session IDs coming from the URL. This is done with three configuration configuration directives: session.use_cookies = 1 tells PHP to set a cookie with a session ID when a session has been started. session.use_only_cookies = 1 means that PHP will only accept session IDs coming from a cookie, not from the URL. And session.use_trans_sid = 0 prevents PHP from automatically inserting the session ID into links.

    Code:
    session.use_only_cookies	1
    session.use_cookies		1
    session.use_trans_sid		0


    Secure session cookies

    Since the cookies carry the session ID, it's very important to protect them. The first thing you should do is activate session.cookie_httponly. This makes the session cookie inaccessible for JavaScript and prevents it from being stolen through a cross-site scripting attack. If you use HTTPS for all user-related content (which you should), then you should also activate session.cookie_secure. This makes sure the cookie will only be transmitted over a HTTPS connection.

    Code:
    session.cookie_httponly		1
    session.cookie_secure		1


    Regenerating the session ID

    When the user logs in, you must generate a new session ID. This prevents session fixation attacks, because the attacker won't know the new ID. It also makes sure that the session ID is actually new and comes from your server. Otherwise, PHP will reuse any value that happens to reside in a cookie called "PHPSESSID" (or whatever name your sessions have).

    PHP Code:
    session_regenerate_id(true);    // the argument tells PHP to delete the old session 


    Session lifetime

    By default, session cookies are destroyed when the users closes the browser. This is generally a good approach, but it does not put an absolute limit on the lifetime of the sessions. You should store the creation time in the session and check it on the server. If it has exceeded, say, 1 hour, destroy the session.

    PHP Code:
    // store current timestamp in session
    $_SESSION['creation_time'] = time(); 
    PHP Code:
    // check if session has expired
    $max_lifetime_seconds 3600;

    $session_lifetime_seconds time() - $_SESSION['creation_time'];
    if (
    $session_lifetime_seconds $max_lifetime_seconds) {
        
    // terminate session



    Protecting the session IDs

    A huge problem of standard sessions is that all IDs are stored on the server as plaintext. If an attacker gets access to the session storage, they can hijack any session they want. To prevent this, the IDs should be hashed. Plain SHA-2 is enough for this, because brute forcing a random session ID is impractical.

    If you're using a custom session handler, it's trivial to use hashed session ID instead of plain IDs. If you're using standard sessions, you can generate an additional random number, put it into a cookie and store its hash in the session:

    PHP Code:
    function secure_random($num_bytes) {
        if (!
    is_int($num_bytes) || $num_bytes <= 0)
            throw new 
    Exception('Argument must be a positive integer.');

        if (
    function_exists('openssl_random_pseudo_bytes'))
            
    $raw_random openssl_random_pseudo_bytes($num_bytes);
        elseif (
    function_exists('mcrypt_create_iv'))
            
    $raw_random mcrypt_create_iv($num_bytesMCRYPT_DEV_URANDOM);
        else
            throw new 
    Exception('OpenSSL or Mcrypt extension required.');

        return 
    bin2hex($raw_random);

    PHP Code:
    // generate session secret, store it in a cookie and store its hash in the session
    $secret secure_random(16);
    $session_cookie_params session_get_cookie_params();

    setcookie('session_secret'$secret$session_cookie_params['lifetime'], $session_cookie_params['path'], $session_cookie_params['domain'], $session_cookie_params['secure'], $session_cookie_params['httponly']);
    $_SESSION['session_secret'] = hash('sha256'$secret); 
    PHP Code:
    // check session secret
    if (!isset($_COOKIE['session_secret'], $_SESSION['session_secret']) || hash('sha256'$_COOKIE['session_secret']) != $_SESSION['session_secret']) {
        
    // terminate session



    Logging out properly

    Terminating a session isn't quite as trivial as it may seem. It actually consists of three different actions: deleting the session data on the server, clearing the $_SESSION array and asking the client to unset the session cookie.

    PHP Code:
    // destroy session data on server
    session_destroy();

    // ask client to delete the session cookie
    $session_cookie_params session_get_cookie_params();
    setcookie(session_name(), ''time() - 24 3600$session_cookie_params['path'], $session_cookie_params['domain'], $session_cookie_params['secure'], $session_cookie_params['httponly']);

    // clear $_SESSION array
    $_SESSION = array(); 


    Usability: No referrer check

    PHP allows you to check the Referer header for additional security. This is a very bad idea. Many privacy-oriented users suppress this header, so they won't be able to log in at all. Deactivate this feature.

    Code:
    session.referer_check		0

    Comments on this post

    • paulh1983 agrees : very good practical advice. Just what i needed :)
    • badger_fruit agrees : Just seen this; Many thanks for the very useful information and time spent compiling it!
    Attached Files
    Last edited by requinix; January 20th, 2014 at 11:26 AM. Reason: secret -> session_secret
    The 6 worst sins of securityHow to (properly) access a MySQL database with PHP

    Why can’t I use certain words like "drop" as part of my Security Question answers?
    There are certain words used by hackers to try to gain access to systems and manipulate data; therefore, the following words are restricted: "select," "delete," "update," "insert," "drop" and "null".
  2. #2
  3. Confused badger
    Devshed Beginner (1000 - 1499 posts)

    Join Date
    Mar 2009
    Location
    West Yorkshire
    Posts
    1,047
    Rep Power
    487
    Thanks for this, I've only just seen it but certainly something worth reading!

    Just one thing, in the php for checking session secret, you're using $_SESSION['session_secret'] but in the declaration in the block above it, you're using $_SESSION['secret'] ... I guess those are supposed to be the same key name?

    Comments on this post

    • Jacques1 agrees
    Last edited by badger_fruit; January 20th, 2014 at 04:32 AM.
    "For if leisure and security were enjoyed by all alike, the great mass of human beings who are normally stupefied by poverty would become literate and would learn to think for themselves; and when once they had done this, they would sooner or later realise that the privileged minority had no function and they would sweep it away"
    - George Orwell, 1984
  4. #3
  5. --
    Devshed Expert (3500 - 3999 posts)

    Join Date
    Jul 2012
    Posts
    3,923
    Rep Power
    1045
    Originally Posted by badger_fruit
    Just one thing, in the php for checking session secret, you're using $_SESSION['session_secret'] but in the declaration in the block above it, you're using $_SESSION['secret'] ... I guess those are supposed to be the same key name?
    Yes. Thanks for pointing this out. I can't edit the post, but I'll ask a moderator to do it.

    Comments on this post

    • requinix agrees : done
    The 6 worst sins of securityHow to (properly) access a MySQL database with PHP

    Why can’t I use certain words like "drop" as part of my Security Question answers?
    There are certain words used by hackers to try to gain access to systems and manipulate data; therefore, the following words are restricted: "select," "delete," "update," "insert," "drop" and "null".
  6. #4
  7. No Profile Picture
    Contributing User
    Devshed Newbie (0 - 499 posts)

    Join Date
    Mar 2014
    Posts
    89
    Rep Power
    1
    Wow, you are a fountain of useful information. thanks Jacques1 :-)
  8. #5
  9. No Profile Picture
    Contributing User
    Devshed Newbie (0 - 499 posts)

    Join Date
    Mar 2014
    Posts
    89
    Rep Power
    1
    Hey jacques, i tried to take the information you said in this thread and apply it to the userCake2.02 system, which is basically a user system template without any real security. just wanted to get your input. this looks correct to me, but i'm not the expert now am i. i use whirlpool as my algorithm of choice.

    if you'll note i had to add the ip and user agent fields to the session object(which uses the alias $loggedInUser). once adding this to the login form, where the object is created upon successful login, it appears to work fine on localhost. admittedly, i have no idea where it is really secure or not, but it seems like it would help against session fixation, since i used your http only directive for cookies, a session hijacking shouldn't be possible, and thus the loop should detect a change in ip/browser agent and prevent the hijacking. am i right or completely wrong?

    PHP Code:
    //session settings
    ini_set('session.cookie_httponly'1);
    ini_set('session.entropy_file''/dev/urandom');
    ini_set('session.hash_function''whirlpool');
    ini_set('session.use_only_cookies'1);
    ini_set('session.cookie_secure'1);
    session_start();
    //Global User Object Var
    if(isset($_SESSION["userCakeUser"]) && is_object($_SESSION["userCakeUser"]))
    {
        
    $loggedInUser $_SESSION["userCakeUser"];//loggedInUser can be used globally if constructed
        
        
    if ($loggedInUser->ip !== get_ip_address() || $loggedInUser->uagent !== getBrowser()) {
            
    // Then it destroys the session
            
    session_unset();
            
    session_destroy();
            
    // Creates a new one
            
    session_regenerate_id(true); // Prevent's session fixation
            
    session_id(sha1(uniqid(microtime()))); // Sets a random ID for the session
        
    }

  10. #6
  11. --
    Devshed Expert (3500 - 3999 posts)

    Join Date
    Jul 2012
    Posts
    3,923
    Rep Power
    1045
    This makes no sense. You've set the entropy source, but you never tell PHP to actually read from it (session.entropy_length). You're not even using the native session IDs. You immediately overwrite them with your own IDs generated with sha1(uniqid(microtime())), which is extremely insecure -- well, it would be if that part of the code wasn't ignored (see below). Those settings actually belong into a php.ini file. Setting them at runtime is a workaround for crappy shared hosts which don't allow you to have one.

    There also seems to be confusion regarding the workflow. The time when the session ID becomes relevant is after the user has authenticated. That's when you generate a fresh ID. It makes no sense to generate one for an unauthenticated visitor, because they aren't gonna need it, anyway.

    You cannot set a session ID or use the session after you've called session_destroy(). This terminates the session mechanism entirely. If you want to do something with the session afterwards, you first have to call session_start() again. So the session_regenerate_id() and session_id() in your code has no effect. It also makes no sense to call both functions: The first gives you an automatically generated ID (which is recommended), the second is for manually setting an ID (which is generally a bad idea). It's obviously not very useful to fetch an ID from PHP only to throw it away and overwrite it with your own ID.

    Last but not least, the whole IP and UA check is very questionable in my mind. An IP doesn't really mean anything. There are thousands of people sharing the same IP due to a proxy, and there may be a single person with dozens of different IP addresses due to an anonymizer like Tor (which is perfectly legitimate). The UA means even less, because anybody can set it to anything. An attacker would simply forge the victim's UA.

    I understand the idea behind this check, and I've seen many people use it. But it has a lot of problems for a very small security benefit. You should rather spend that time on double-checking the settings and the workflow.
    The 6 worst sins of securityHow to (properly) access a MySQL database with PHP

    Why can’t I use certain words like "drop" as part of my Security Question answers?
    There are certain words used by hackers to try to gain access to systems and manipulate data; therefore, the following words are restricted: "select," "delete," "update," "insert," "drop" and "null".
  12. #7
  13. No Profile Picture
    Contributing User
    Devshed Newbie (0 - 499 posts)

    Join Date
    Mar 2014
    Posts
    89
    Rep Power
    1
    that's why i asked you, but instead you just post a wall of text and i'm now even more confused, because you're just dancing around the questions.

    please show me the proper way so that i can no. you tell me not to roll my own security, but you won't help at all. seems to me the only thing you do here is stroke your own ego.
  14. #8
  15. No Profile Picture
    Contributing User
    Devshed Newbie (0 - 499 posts)

    Join Date
    Mar 2014
    Posts
    89
    Rep Power
    1
    ok, about the only information i was able to gain from your post is to remove the line where i generate the random id(got that from a question off stack overflow.) i see now that if i leave it alone it will just use the directives in the ini set? am i right about that much at least? this session stuff really confuses me for some reason. in fact, about the only thing in programming that confuses me is the OWASP top 10 and how to prevent/mitigate.
  16. #9
  17. --
    Devshed Expert (3500 - 3999 posts)

    Join Date
    Jul 2012
    Posts
    3,923
    Rep Power
    1045
    First of all: Fix your attitude. I've spent quite some time trying to help you and writing down detailed explanations. If all I get back is a bunch of insults and “Too long, didn't read”, I rather do something else.

    If you won't even spend 15 minutes to try and understand a short text, forget about programming. This is not for the impatient. I'm happy to answer concrete questions if there's something you don't understand. But I'm not gonna break this down into a Tweet-sized code snippet for you to copy and paste.

    So what's your decision? Are you willing to spend some time on the topic or not?
    The 6 worst sins of securityHow to (properly) access a MySQL database with PHP

    Why can’t I use certain words like "drop" as part of my Security Question answers?
    There are certain words used by hackers to try to gain access to systems and manipulate data; therefore, the following words are restricted: "select," "delete," "update," "insert," "drop" and "null".
  18. #10
  19. No Profile Picture
    Contributing User
    Devshed Newbie (0 - 499 posts)

    Join Date
    Mar 2014
    Posts
    89
    Rep Power
    1
    ok, i'll fix my attitude if you'll fix your tone. you are very condescending imo(you talk as if im a dog, or will never be worthy, slightly annoying).

    i have a question about your previous reply, i will try to quote everything this time so it will be easier for you to comprehend.

    This makes no sense. You've set the entropy source, but you never tell PHP to actually read from it (session.entropy_length).
    How do i tell php to read from it?

    If you want to do something with the session afterwards, you first have to call session_start() again.
    from this, i understand that i need to call session start after destroying the previous session. does this mean that session_regenerate_id is no longer needed?(my brain tells me it wouldn't be needed since i just started a new one, but i've learned to never assume anything in php.)

    Last but not least, the whole IP and UA check is very questionable in my mind. An IP doesn't really mean anything. There are thousands of people sharing the same IP due to a proxy, and there may be a single person with dozens of different IP addresses due to an anonymizer like Tor (which is perfectly legitimate). The UA means even less, because anybody can set it to anything. An attacker would simply forge the victim's UA.
    I understand your point and its valid, but my ipaddress function does adequate checks to detect proxies. also, i read an array of tor exit nodes from memcached and block them from the application entirely.)

    If the attacker, forges the ua and ip, the ip check catches the forgery because i do not rely on any particular method for determining the ip address. each function hashes the ua + signup timestamp of the user with bcrypt(provable secure) and a RFC compliant salt(also provably secure). it is highly unlikely the attacker is able to steal the session in one try without having access to the database, which at that point i would be ****ed anyway.

    I look forward to your response.
  20. #11
  21. Wiser? Not exactly.
    Devshed God 1st Plane (5500 - 5999 posts)

    Join Date
    May 2001
    Location
    Bonita Springs, FL
    Posts
    5,905
    Rep Power
    3969
    Originally Posted by r3wt
    How do i tell php to read from it?
    By setting the session.entropy_length value to something other than 0 (which is the default).
    Originally Posted by PHP Manual
    session.entropy_length integer
    session.entropy_length specifies the number of bytes which will be read from the file specified above. Defaults to 0 (disabled).

    Originally Posted by r3wt
    from this, i understand that i need to call session start after destroying the previous session. does this mean that session_regenerate_id is no longer needed?
    session_start() will use the ID given to it in the request if one is available. session_destroy does not clear this information out so session_start would end up just re-using the same ID. You will still need to call session_regenerate_id() in order to change the ID to a new value. If all you want to do is regenerate the ID and clear out the data on login, calling session_destroy is unnecessary though. Simply call session_regenerate_id and assign an empty array to $_SESSION.
    Code:
    session_start();
    session_regenerate_id(true);
    $_SESSION=array();
    Recycle your old CD's, don't just trash them



    If I helped you out, show some love with some reputation, or tip with Bitcoins to 1N645HfYf63UbcvxajLKiSKpYHAq2Zxud
  22. #12
  23. --
    Devshed Expert (3500 - 3999 posts)

    Join Date
    Jul 2012
    Posts
    3,923
    Rep Power
    1045
    Originally Posted by r3wt
    How do i tell php to read from it?
    With the setting I wrote down in the sentence you're quoting: session.entropy_length.

    This specifies the number of bytes which should be read from the entropy source and mixed into the ID. 16 is a good value (it's also in the first post as an example).



    Originally Posted by r3wt
    from this, i understand that i need to call session start after destroying the previous session. does this mean that session_regenerate_id is no longer needed?(my brain tells me it wouldn't be needed since i just started a new one, but i've learned to never assume anything in php.)
    Yes, it's not needed at this point.

    When you terminate a session due to your check, that means you do not recognize the visitor as a valid user. Why would you generate a fresh ID for a random visitor you don't even know? What are they supposed to do with it?

    The time to refresh the ID is after the visitor has logged in. That's when the session actually becomes important and must be protected from the various attacks.



    Originally Posted by r3wt
    I understand your point and its valid, but my ipaddress function does adequate checks to detect proxies. also, i read an array of tor exit nodes from memcached and block them from the application entirely.)
    No offense, but blocking all proxies and Tor users is just incredibly stupid. If your website is so famous that people will accept any rule you set up, you may get away with this. Otherwise, you'll simply lose a lot of legitimate users.

    Proxies, VPNs and Tor are a natural part of the Internet. They are used for all kinds of reasons: privacy, security, company policies, technical conditions etc. There's absolutely no good reason whatsoever for blocking them.

    This won't even stop attackers, if that's what you're trying to do. Those people use botnets, not some official proxy.





    Originally Posted by r3wt
    If the attacker, forges the ua and ip, the ip check catches the forgery because i do not rely on any particular method for determining the ip address. each function hashes the ua + signup timestamp of the user with bcrypt(provable secure) and a RFC compliant salt(also provably secure).
    This sounds an awful lot like snake oil. I don't even know what the term “RFC-compliant salt” is supposed to mean.

    The UA is neither secret nor protected. Any vulnerability which reveals the session ID would hand out the UA for free. This is zero effort for an attacker, no matter how many bcrypts and salts you add to the scenario.

    I wouldn't waste my time with this. Security comes from getting the architecture right and avoiding fundamental mistakes, not from adding all kinds of obscure checks (which increase complexity and may actually weaken the overall system).
    The 6 worst sins of securityHow to (properly) access a MySQL database with PHP

    Why can’t I use certain words like "drop" as part of my Security Question answers?
    There are certain words used by hackers to try to gain access to systems and manipulate data; therefore, the following words are restricted: "select," "delete," "update," "insert," "drop" and "null".

IMN logo majestic logo threadwatch logo seochat tools logo