PHP Sessions across sub domains 2

2019-05-17 17:35发布

问题:

This is a complement of PHP Sessions across sub domains
I tried what is indicated on that question, and I see that the issue wasn't given.

So I need to have sessions across sub-domains (www.example.com to forum.example.com)

What I did on www.example.com is

session_name("a_name");
session_set_cookie_params(0, '/', '.example.com');
session_start();

echo session_id();
$_SESSION['test'] = 123;

On forum.example.com

session_name("a_name");
session_set_cookie_params(0, '/', '.example.com');
session_start();

echo session_id();
print_r($_SESSION);

The session_id are exactly the same, but the $_SESSION doesn't output anything.
How to make forum.example.com output 123 ?

I tried session.cookie_domain = .example.com but doesn't change anything

When I go on forum.example.com it destroys the www.example.com sessions, and it does the same on the other way, like if it detects that it comes from another sub-domain and erases everything for security.

The 2 sub-domains are on the same Debian server

Another thing that I noticed is that without session_name and session_set_cookie_params it still has exactly the same session_id, when I set session.cookie_domain

Thank You

回答1:

Ok, I've thought about this for a while and I think I've got it.

First things first: since you are getting the same session id from both servers, we can rule out any cookie-related issues. Clearly, you are successfully creating a cookie named a_name (though I'd recommend only alphanumeric characters for that cookie name) on www.example.com, and successfully reading that a_name cookie on forum.example.com. But, like you said, you aren't getting any data from forum.example.com. The session.cookie_lifetime = 0 is not an issue: that just means that the session cookie remains until the browser is closed.

We should delve into PHP's session handling a bit further. The session id you are reading out with session_id() refers to a file on your server. Typically, that file is present in /tmp/sess_$session_id. The contents of that file are your $_SESSION array, serialized. (Keep in mind that the data is not serialized the same way that serialize() in PHP does... but that's not important right now.).

I think this is a file permission-related issue:

  1. /tmp/sess_$session_id file is set with www.example.com's user and group.
  2. forum.example.com attempts to open /tmp/sess_$session_id, but doesn't have the proper permissions.
  3. As a result, you get an empty result when trying to print_r($_SESSION);

Solution:
Check your server's configuration file to make sure that www.example.com and forum.example.com are running as THE SAME USER AND GROUP. That is critical! For Apache, find your *.conf file:

User youruser
Group yourgroup

For nginx, find nginx.conf:

user youruser yourgroup;

If changing the server config files is not an option, then you should make sure that the users running the two sites are in the same group.

You can verify that this is the problem by first loading www.example.com and then sudo ls -ltc sess_* in your server's shell, via SSH (find the sess_ ending in your $session_id). Next, load forum.example.com and then sudo ls -ltc sess_* again, to see the user and/or group change.



回答2:

For this answer I've made a few assumptions:

  • The user has to enter there credentials at least once on each domain (any other way would be a serious security issue)
  • You have access to either a database or file space outside the web root.
  • sub-domain, domain or any other name will be referenced as "site"
  • The aim is to have a common file (physical file or serialized in database) accessible from each site/domain.

For my example I will be using a database, since it's the idea I'm putting across, not database/file access techniques, I will have unnecessary lines removed, IE: How to connect to the database.

If this concept is what you were after, or if anyone else want me to fill in the blanks for completeness, just leave a comment. On with the code.


I would take a completely different approach.
From what I gather from your question, and the related post you linked to, you are trying to share a session using a common session name.

  1. Each site has it's own session id.
  2. Each site has it's own authentication cookie ( $_COOKIE['userid'] or $_COOKIE['userhash'] ).
  3. Individual sessions are created, and a common cookie is stored on each site.
    • Using a custom session handler each site reads the same data. class MySessionHandler implements SessionHandlerInterface
    • My after thought was an even simpler approach, a class that acts like a session handler, reading / writing to a common file. Since php's session handler doesn't save the data until the script has ended.

Original idea - Won't go into details, it's just for reference.

class MySessionHandler implements SessionHandlerInterface {
    private $savePath;

    public function read($id) {
        $id   = some_user_authentication_function();
        $hash = $_COOKIE['_h'];

        $result = mysql_query("SELECT sess_data FROM login_table WHERE user_id = {$id} AND hash = {$hash}");
        return $result['sess_data'];
    }

    public function write($id, $data) {
        $id   = some_user_authentication_function();
        $hash = $_COOKIE['_h'];

        $result = mysql_query("UPDATE login_table SET sess_data = {$data} WHERE user_id = {$id} AND hash = {$hash}");

        return ($result === false) ? false : true;
    }
}

$handler = new MySessionHandler();
session_set_save_handler($handler, true);
session_start();



class customSessionHandler
{
    private $hash;
    private $id;
    private $sess_db;

    public function __construct($db) {
        $this->hash    = $_COOKIE['hash'];
        $this->id      = some_user_authentication_function($this->hash);
        $this->sess_db = $db;
    }

    public function get($key) {
        $query = 
            "SELECT value ".
            "FROM ".$this->sess_db.
            "WHERE user_id = {$id} ".
            "    AND hash = {$hash} ".
            "    AND key = {$key}";

        $result = mysql_query($query);
        return $result['key'];
    }

    public function set($key, $val) {
        $query = 
            "REPLACE INTO ".$this->sess_db.
            "SET {$key} = {$val} ".
            "WHERE user_id = {$id} ".
            "    AND hash = {$hash}";

        return (mysql_query($query) === false) ? false : true;
    }
}
$handler = new customSessionHandler('sess_data');
session_start();



As stated at the beginning, any code that isn't essential to explaining the concept has been removed.
Things that might not be obvious to everyone: - $key and $val need to be sanitized before sending to the database. (prevent injection attacks) - The hash gets sent to your login functions, so the hash can be used to clear the session data when needed, can also be used in the authentication of the user. - mysql prepared statements would be ideal here, so you can prepare the two queries in the constructor, then you just reuse the statement on every call. Then put the connection close code in the destructor.


After thought

There would be much greater security if each site had it's own hash.
Then if you detect a security anomaly, you can just block or re-request the credentials from the one site, without compromising the hash for the network of sites.

To implement this would be as easy as setting up another table containing: - user_id - site_name (example.com) - hash - timeout - re-authenticate

and modifying the session_data table, so instead of accessing the $key => $val pair by hash, you access it by user_id.


Thanks for reading, hopefully it will be of use to someone.