getJSON and session_regenerate_id()
Asked Answered
T

1

8

I am performing a standard getJSON query from a page which is session protected:

$.getJSON('queries.php',{q: 'updateEvent', param1: p1},
    function(data){
        ...
    }
);

On my session constructor I have set the following :

function startSession() 
{
    ini_set('session.use_only_cookies', SESSION_USE_ONLY_COOKIES);

    $cookieParams = session_get_cookie_params();
    session_set_cookie_params(
        $cookieParams["lifetime"], 
        $cookieParams["path"], 
        $cookieParams["domain"], 
        SESSION_SECURE, 
        SESSION_HTTP_ONLY
     );

    session_start();

    if ( SESSION_REGENERATE_ID )
        session_regenerate_id(SESSION_REGENERATE_ID);   
}

If I set SESSION_REGENERATE_ID to true, then my getJSON sends a token, but receives a different one, making the request fail. So for the moment I'm dealing with SESSION_REGENERATE_ID set to false.

Is there a way to make getJSON work in such conditions ?

EDIT : all files are under the same domain.

We have index.php where the js is included, we have queries.php which is the php file called by the ajax requests, we have s_session.php which includes the constructor written above.

Files index.html and queries.php are both protected at the begining this way :

include "s_session.php"; 
if(!$login->isLoggedIn()) {
  header('Content-Type: application/json'); 
  echo json_encode(array('content' => 'Login failed')); 
  exit;
}

The PHPSESSID is in the header of the ajax request under set-cookie. The PHPSESSID returned in the answer is different, as expected from session_regenerate_id.

If SESSION_REGENERATE_ID is set to FALSE the requests are going through without problem. If it is set to TRUE, then I get the error message "Login failed".

Here is the isLoggedIn() :

public function isLoggedIn() {
    //if $_SESSION['user_id'] is not set return false
    if(ASSession::get("user_id") == null)
         return false;

    //if enabled, check fingerprint
    if(LOGIN_FINGERPRINT == true) {
        $loginString  = $this->_generateLoginString();
        $currentString = ASSession::get("login_fingerprint");
        if($currentString != null && $currentString == $loginString)
            return true;
        else  {
            //destroy session, it is probably stolen by someone
            $this->logout();
            return false;
        }
    }

    $user = new ASUser(ASSession::get("user_id"));
    return $user->getInfo() !== null;
}

EDIT 2 : Here is the full ASSession code :

class ASSession {

/**
 * Start session.
 */
public static function startSession() 
{
    ini_set('session.use_only_cookies', SESSION_USE_ONLY_COOKIES);

    session_start();
    $s = $_SESSION;

    $cookieParams = session_get_cookie_params();

    session_set_cookie_params(
        $cookieParams["lifetime"], 
        $cookieParams["path"], 
        $cookieParams["domain"], 
        SESSION_SECURE, 
        SESSION_HTTP_ONLY
     );

    if ( SESSION_REGENERATE_ID )
        session_regenerate_id(SESSION_REGENERATE_ID);

    //$_SESSION = $s;

}

/**
 * Destroy session.
 */
public static function destroySession() {

    $_SESSION = array();

    $params = session_get_cookie_params();

    setcookie(  session_name(), 
                '', 
                time() - 42000, 
                $params["path"], 
                $params["domain"], 
                $params["secure"], 
                $params["httponly"]
            );

    session_destroy();
}

/**
 * Set session data.
 * @param mixed $key Key that will be used to store value.
 * @param mixed $value Value that will be stored.
 */
public static function set($key, $value) {
    $_SESSION[$key] = $value;
}

/**
 * Unset session data with provided key.
 * @param $key
 */
public static function destroy($key) {
    if ( isset($_SESSION[$key]) )
        unset($_SESSION[$key]);
}

/**
 * Get data from $_SESSION variable.
 * @param mixed $key Key used to get data from session.
 * @param mixed $default This will be returned if there is no record inside
 * session for given key.
 * @return mixed Session value for given key.
 */
public static function get($key, $default = null) {
    if(isset($_SESSION[$key]))
        return $_SESSION[$key];
    else
        return $default;
}

}

EDIT 3: here are the request headers and response cookie :

enter image description here enter image description here

I noticed that the very first getJSON which is performed during the onload is successfull. All the others done after and triggered by user are unsuccessfull

Timehonored answered 25/1, 2016 at 12:55 Comment(15)
I understand as you commented in my answer (I deleted it now, will undelete, if I can find any other solution for you). So, how does your token actually works? If session data exists, then it will only change the session_id, all others should remain same. Why it is sending wrong token? Is it just sending the session_id as token?Assuasive
yes ending only the session_id. I have read that getJSON doesn't send credentials by default. That's why ajax got a xhrField called withCredentials which should be set to true. But I don't know how to include it in getJSON as there is no syntax on the official doc to include xhrFields specificallyTimehonored
to be more specific, the php page returns an error message that the login was impossible (therefore not recognising session data). In the console I see that the request headers set-cookie is sending one PHPSESSID, but the response PHPSESSID, which is coming with the error message, is differentTimehonored
If that fixes your problem just use $.ajax function and use the extra parameter dataType:"json" to make it work like $.getJSON. Like this $.ajax({url:url,xhrFields: {withCredentials: true }, dataType:"json"}); Let me know, if that works for you.Assuasive
set-cookie is sending one PHPSESSID, but the response PHPSESSID, which is coming with the error message, is different, that's what session_regenerate_id() must do. It's changing the PHPSESSID actually. So, why do you call that function if you don't want to change the PHPSESSID?Assuasive
withCredentials : doesn't work :/ Ok, was not really sure how the cookie would look like. I want to perform a getJSON call and regenerate the session id together.... but getJSON doesn't work. It only works when I set session_regenerate_id to false :/Timehonored
Let us continue this discussion in chat.Assuasive
What do you mean by doesn't work actually? You get some error?Assuasive
I'm confused as well. So these pages all live under one domain. So when you call your AJAX and regenerate the session it should update the cookie and your root page will be affected by that. Are you explicitly passing the PHPSESSID?Euphroe
let me edit the question to show more detailsTimehonored
I guess something wrong with the way ASSession works. So we need more and more code to find the reasons! :)Assuasive
Is that only a single $.getJSON() call or multiple ones? And when exactly are those calls executed? During the page load or after that? If there are multiple calls, are they executed simultaneously and do they all fail? Please post the relevant Request & Response headers.Bidentate
It is several calls, made one by one on user action, never simultaneously. They all fail the same way. I'll post the request and response headers in a few hours. ThanksTimehonored
@Bidentate here are the headers and responseTimehonored
Out of curiosity: Have you checked if the domain you are logged in and the domain the AJAX request is calling is the same? (same subdomain also, eg. google.com != www.google.com)Loath
B
2

This is mostly caused by a race condition, but a browser bug is also possible.

I would rule out the browser bug scenario, but there's a conflict in the provided info, more specifically in this comment:

It is several calls, made one by one on user action, never simultaneously.

If the requests are never executed simultaneously, then that could only mean that your browser is not functioning properly and one of the following happens:

  • Discarding the Set-Cookie header it receives in the response (if that logic depends on the HttpOnly flag, this would explain why the web still works :D)
  • The onLoad event in fact executed during the page load (I know that doesn't make sense, but everything's possible if it's a browser bug)

Of course, those are highly unlikely to happen, so I'm inclined to say that you are in fact processing multiple AJAX requests at a time, in which case the race condition is a plausible scenario:

  1. First request starts (with your initial PHPSESSID)
  2. Second request starts (again, with the same PHPSESSID)
  3. First request is processed and receives response with a new PHPSESSID
  4. Second request was blocked until now (the session handler uses locking to prevent multiple processes modifying the same data simultaneously) and is just now starting to be processed with the initial PHPSESSID, which is invalid at this point, hence why it triggers a log-out.

I would personally look at what gets triggered by that onLoad event - it's easy to just put all initialization logic in there and forget that this may include multiple asynchronous requests.


Either way, the real logical error on your part is this piece of code:

if ( SESSION_REGENERATE_ID )
    session_regenerate_id(SESSION_REGENERATE_ID);

You're using the same value for what are two different conditions:

  1. Determining whether to regenerate the session ID at all
  2. Telling session_regenerate_id() if it should immediately destroy data associated with the old session ID

The option not do destroy that data immediately is there exactly to provide a solution to these race conditions with asynchronous requests, because they are practically unavoidable. A race condition will happen at some point, regardless of how hard you try to avoid it - even if there's no logical flaw, network lags (for example) may still trigger it.
Preserving the old session's data (temporarily of course) works around that problem by simply allowing a "late" or "out of sync" request to work with whatever data was available at the time it was started.

Expired sessions will be cleaned up later by the session garbage collector. That's possibly not ideal, but pretty much the only solution for storages that require you to delete data (as opposed to cache stores like Redis, which allow you to set a TTL value instead of having to manually delete).

Personally, I prefer to avoid session ID regeneration specifically during AJAX requests ... as you can see, it's a can of worms. :)

Bidentate answered 29/1, 2016 at 19:26 Comment(10)
Thanks a lot for the answer! Very detailed and useful! No browser bug for sure. The race condition is very likely to happen. Your answer makes me understand why the initial Ajax request succeeded and why the others failed. As explained before in the comments, session_regenerate_id does not destroy the session data, only change PHPSESSID. So just by curiosity, is there a way, if we have multiple Ajax requests at the same/or at a close time happening, to make them successful.... for ex conditioning the session regenrate to some timer... like 1s, so all requests have time to finish ?Timehonored
I've noticed that the first user action is triggering 2 getJSON actions at the same time.... after that it is only one by one, but I guess since the first action made the system fail, the later ones are meant to failTimehonored
"As explained before in the comments, session_regenerate_id does not destroy the session data, only change PHPSESSID" - the thing is ... that data is carried over with the new session ID, but it's a copy of the old data. When you call session_regenerate_id(true), the old copy is destroyed, which is what invalidates the session. Just change it to false and leave the garbage collector to its job. :)Bidentate
ok I got it! Thanks a lot for your time and the very clear explanation !Timehonored
Is there a way to filter getJSON and other requests in order to trigger session_regenerate_id ?Timehonored
Yes, most JS frameworks (jQuery included) send a X-Requested-With: XMLHttpRequest header with ajax requests - you can check for that. But be careful - that also allows anybody to prevent regenerating session IDs on every request, so you also need to implement logic that regenerates at all costs after a certain amount of time.Bidentate
Ok got it. Stupid question but does it mean that despite my addresses are in https, the Ajax requests are made in http?Timehonored
No. :) You can see this (and the X-Requested-With header) in your own screenshot.Bidentate
Well the screenshot shows me XMLHttpRequest.... doesn't look like https ... but that's just a guess ;)Timehonored
It shows a :scheme: line and that your cookie has the secure flag, meaning that you wouldn't even see it if it wasn't transmitted over https.Bidentate

© 2022 - 2024 — McMap. All rights reserved.