cytodev
cytodev

Reputation: 87

Session missing except when I use an "unsafe" cookie

Preface

These answers did not help me at all, but they might help someone who stumbles upon this question:


The question

I have been breaking my peanut over this all day yesterday and I can't seem to catch a lucky break when it comes to logging into my own application. All helpful answers on SO are along the lines of "Did you start a session?" - Which I have.

When I log in, the script returns with status correct and gives me the following headers:

"Expires: Thu, 19 Nov 1981 08:52:00 GMT",
"Cache-Control: no-store, no-cache, must-revalidate",
"Pragma: no-cache",
"Set-Cookie: ADMSESSION=f27n49icvbv0bjfgh4bffojgs0; path=\/; secure; HttpOnly"

I can verify that this cookie is indeed present in my browser, and the cookie is terminated only when the browser closes.

Now this is where the magic doesn't happen. After receiving the go ahead status from the server the local script refreshes the page in order to load the real back-end session. At least, that's what it's supposed to do. The script reloads the page, and I am greeted by the same login prompt again. It's driving me up the wall.

After careful examination I found that my session cookie was not being used at all, so I changed it over to an insecure cookie (Set-Cookie: ADMSESSION=f27n49icvbv0bjfgh4bffojgs0). This seems to work on all pages that require the user to be logged in... However, it only works on those pages, and not on the login page. I cannot wrap my head around this, since it all follows the same trajectory in the logic and no headers are sent until the server is completely done with the processing.


Relevant snippets

Here are some relevant snippets that illustrate the logic-flow and way of setting and getting the session. Some information has been redacted for security purposes.

<?php // index.php

    /**
     * just print everything, I want to know when, where, and how
     * the brown sticky stuff starts hitting the ceiling whirly device.
     */
    error_reporting(E_ALL);
    ini_set("display_startup_errors", 1);
    ini_set("display_errors", 1);

    require_once "path/to/my/config.php";

    // session and login manager
    require_once "path/to/my/SecurityService.php";

    use \Namespace\Of\My\SecurityService as LoginManager;

    // argument false: don't log activity on this page.
    $loginManager = new LoginManager(false);

    // start session
    $loginManager->secureSessionStart();

    // @fixme: secureSessionCheck is not fired!?
    if($loginManager->secureSessionCheck()) {
        print file_get_contents("path/to/views/main.html");
    } else {
        print file_get_contents("path/to/views/login.html");
    }

<?php // SecurityService.php:51-75 (inside login method called via POST)
        if($this->verify($credentials["passwordHash"])) {
            if($this->logger !== null)
                $this->logger->write("info", "successful login from {$_SERVER["REMOTE_ADDR"]}.");

            $_SESSION["browser"] = $_SERVER["HTTP_USER_AGENT"];
            // redacted
            // redacted
            $_SESSION["visitor"] = $_SERVER["REMOTE_ADDR"];
            // redacted
            // redacted
            // redacted
            // redacted
            // redacted
            // redacted
            // redacted
            // redacted
            // redacted

            session_commit();

            if($this->logger !== null)
                $this->logger->write("info", "User ({$_SESSION["email"]}) created in session (" . session_id() . ").");

            return true;
        }

<?php // SecurityService.php:92-127
    /**
     * secureSessionStart
     *   Starts a session in a secure way
     */
    public function secureSessionStart() {
        if(ini_set("session.use_only_cookies", 1) === FALSE) {
            if($this->logger !== null)
                $this->logger->write("error", "PHP ini is configured incorrectly. (session.use_only_cookies)");

            return;
        }

        switch(session_status()) {
            case PHP_SESSION_DISABLED:
                if($this->logger !== null)
                    $this->logger->write("error", "Sessions are disabled. Unable to log this user in!");
                return;
            case PHP_SESSION_NONE:
                session_set_cookie_params(0, "/", "", true, true);
                session_name("ADMSESSION");
                session_start();
                break;
            case PHP_SESSION_ACTIVE:
                session_set_cookie_params(0, "/", "", true, true);
                session_name("ADMSESSION");
                session_start();

                $oldID = session_id();

                session_regenerate_id(true);

                if($this->logger !== null)
                    $this->logger->write("info", "Session ({$oldID}) moved to (" . session_id() . ").");
                break;
        }
    }

<?php // SecurityService.php:118-189
    /**
     * secureSessionCheck
     *   Checks the current session if it"s valid with current data
     *
     * @return boolean
     */
    public function secureSessionCheck() {
        if(!isset($_SESSION)) {
            if($this->logger !== null)
                $this->logger->write("error", "Session is not set.");

            return false;
        }

        if(!isset($_SESSION[/* redacted */]) || !isset($_SESSION["browser"]) || !isset($_SESSION["visitor"])) {
            if($this->logger !== null)
                $this->logger->write("error", "Session (" . session_id() . ") does not contain a valid administrator.");

            return false;
        }

        if($_SESSION["browser"] !== $_SERVER["HTTP_USER_AGENT"]) {
            if($this->logger !== null)
                $this->logger->write("warning", "Session (" . session_id() . ") browser conflicts with current user agent.");

            return false;
        }

        if($_SESSION["visitor"] !== $_SERVER["REMOTE_ADDR"]) {
            if($this->logger !== null)
                $this->logger->write("warning", "Session (" . session_id() . ") visitor conflicts with current visitor IP.");

            return false;
        }

        // redacted (gets $this->data)

        if(!isset($this->data)) {
            if($this->logger !== null)
                $this->logger->write("error", "Data object is not set.");

            return false;
        }

        if(!isset($this->data->/* redacted */) || !isset($this->data->/* redacted */) || !isset($this->data->/* redacted */) || !isset($this->data->/* redacted */)) {
            if($this->logger !== null)
                $this->logger->write("error", "Data object does not contain a valid administrator.");

            return false;
        }

        $compare = // redacted
            // redacted
            // redacted
            // redacted
            // redacted
            // redacted
            // redacted
            // redacted
        // redacted

        $isValid = hash_equals($compare, $_SESSION[/* redacted */]);

        if(!$isValid) {
            if($this->logger !== null)
                $this->logger->write("error", "Illegal session presented by {$_SERVER["REMOTE_ADDR"]}.");

            return false;
        }

        return true;
    }

Snippet of my log (after logging in once)

2016-11-01 09:18:55 – [info] Session (359hk6v83pc3a82b9tlbn79ch5) moved to (6g05qcg7pocaht66ru5gdq8di6).
2016-11-01 09:18:55 – [info] successful login from 127.0.0.1.
2016-11-01 09:18:55 – [info] User ([email protected]) created in session (6g05qcg7pocaht66ru5gdq8di6).
2016-11-01 09:19:08 – [info] Session (6g05qcg7pocaht66ru5gdq8di6) moved to (jo0etnds61ip2ices59hrg1jl6).  

The strange thing in the last snippet is that it does not log anything after moving the session to a new ID. I would expect (as is the case when I try to access pages without logging in) at least the following in my log: 2016-11-01 10:08:47 – [error] Session (jo0etnds61ip2ices59hrg1jl6) does not contain a valid administrator. - Strange behavior on the login page...


Questions and answers

Q: Have you checked the browser console? Any warnings relating to HTTPS in there? Maybe a mixed content warning or something like that? – CBroe
A: None whatsoever. The login page loads without any issues, sends the data over to the server, and the server responds correctly. No console messages can be found relating to HTTPS. The network tab also does not show any status other than 200. Login is called once (via the POST request) and the page refreshes afterwards. I can trace the session cookie like so: 1 (initial load) -> none sent, 1 received (071s21bdejg40supml9kamced5). 2 (POST to login) -> none sent, 1 received (813um7v1jjh3jsi57g5k8evm16) - This shouldn't be an issue, right? Since the user should be logged in to the new session ID. 3 (reload) -> 1 sent (071s21bdejg40supml9kamced5), 1 received (k5aa0o4k24v2cfc41qbleq04h4)...

The actual issue has been found by checking the cookies sent/received. The fix to this issue has not.


Current understanding of the problem

The issue was found based the steps I took after reading CBroe's comment.

It seems that I was getting the wrong session cookie back from the server, and I believe that I can attribute that to the secureSessionStart function. I removed setting of cookies (for testing purposes) and and I am logged in correctly. However, this defeats the entire purpose of using secure sessions with cookies that cannot be accessed by script. When I simply use session_start() all is well and I am greeted by the actual back-end of the application. Whenever I use session_set_cookie_params(0, "/", "", true, true) the login page pops up again and again.


Final thoughts

It's probably a missing semicolon somewhere. It's always a missing semicolon...

Upvotes: 3

Views: 729

Answers (1)

cytodev
cytodev

Reputation: 87

Apparently, my server did not receive the secure headers and this was causing my login session to be lost on reload. I have currently fixed this by checking if the connection is secured before setting the session assuming it is.


Disclaimer

I DO NOT advise using an insecure connection to handle your login requests! If you're having the same issue I've been having, be sure to check that the connection is secure all the way trough before turning it off in your session cookie!


The current fix

Two small additions have been made to the secureSessionStart method in order to add the domain name and security to the session cookie. Please always make sure the connection is secure. This should be for testing purposes only!

/**
 * get's the secure status from _SERVER
 *   Also check against HTTP_X_FORWARDED and HTTP_X_FORWARDED_SSL because
 *   some servers are behind load balancers.
 */
$secure = (isset($_SERVER["HTTPS"]) && $_SERVER["HTTPS"] === "on") && ((!empty($_SERVER["HTTP_X_FORWARDED_PROTO"]) && $_SERVER["HTTP_X_FORWARDED_PROTO"] === "https") || (!empty($_SERVER["HTTP_X_FORWARDED_SSL"]) && $_SERVER["HTTP_X_FORWARDED_SSL"] === "on"));

// also use _SERVER["SERVER_NAME"] to set the cookie to this domain only
session_set_cookie_params(0, "/", $_SERVER["SERVER_NAME"], $secure, true);  

The ideal fix

The ideal fix is to just use SSL on every page that requires it. The current domain I'm working with does not have this set up, and I'm fixing that as I'm typing this. I'm not even joking. Get your SSL up before building your login system.

Upvotes: 2

Related Questions