I have upgraded from PHP 5.6 to 7 and use a custom session handler script to manage sessions in my database so I can have better control over my users. Everything works fine as before except one thing.
session_regenerate_id(true);
After a user successfully logs in, to avoid session fixation, we call session_regenerate_id. Worked fine with PHP 5.6, not so with PHP 7:
[14-May-2016 02:04:07 UTC] PHP Warning: Uncaught Error: Call to a member function prepare() on null in /home/xxxx/protected/class.database.php:38 Stack trace:
#0 /home/xxxx/protected/class.session.php(37): Database->query('SELECT data FRO...')
#1 [internal function]: Session->_read('kuq04akaagkjd5n...')
#2 /home/xxxx/protected/user_auth_fns.php(705): session_regenerate_id(true)
#3 /home/xxxx/public_html/passengersdir/login.php(42): login_user('xxxx', 'xxxx', 'xxxx', NULL)
#4 {main}
thrown in /home/xxxx/protected/class.database.php on line 38 [14-May-2016 02:04:07 UTC] Recoverable error: session_regenerate_id(): Failed to create(read) session ID: user (path: ) in /home/xxxx/protected/user_auth_fns.php on line 705
Apparently session_regenerate returns false but PHP 5.6 let it slide. This is the official response from PHP:
FALSE
/failure means "Something wrong in read" such as permission/network/etc errors.https://bugs.php.net/bug.php?id=71187
Some people found a workaround here: https://github.com/magento/magento2/issues/2827
It appears to be an issue with the read function not always returning a string. As a temporary fix I cast the return value for the read() function in the SessionHandler as a string.
I don't like workarounds. I can simply remove session_generate_id
and it works like the good old times, but you open to session fixation.
Ideally I would like to solve this issue OR find a better way to fight session fixation after successful login. Thank you for looking.
Here is the code in question:
public function __construct(){ // this is class.database.php
// Set DSN
$dsn = 'mysql:host=' . $this->host . ';dbname=' . $this->dbname;
// Set options
$options = array(
PDO::ATTR_PERSISTENT => true,
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
);
// Create a new PDO instanace
try{
$this->dbh = new PDO($dsn, $this->user, $this->pass, $options);
}
// Catch any errors
catch(PDOException $e){
$this->error = $e->getMessage();
}
}
public function query($query){
// this throws Uncaught Error: Call to a member function prepare() on null in /home/opentaxi/protected/class.database.php:38
$this->stmt = $this->dbh->prepare($query);
}
public function bind($param, $value, $type = null){
if (is_null($type)) {
switch (true) {
case is_int($value):
$type = PDO::PARAM_INT;
break;
case is_bool($value):
$type = PDO::PARAM_BOOL;
break;
case is_null($value):
$type = PDO::PARAM_NULL;
break;
default:
$type = PDO::PARAM_STR;
}
}
$this->stmt->bindValue($param, $value, $type);
}
public function execute(){
return $this->stmt->execute();
}
// this is from class.session.php
public function _read($id){
//this is /home/opentaxi/protected/class.session.php(37): Database->query('SELECT data FRO...')
$this->db->query('SELECT data FROM sessions WHERE id = :id');
$this->db->bind(':id', $id);
if($this->db->execute()){
$row = $this->db->single();
return (string)$row['data']; // I have the workaround that's not working!
}else{
return '';
}
}
Your custom handler MUST return a string ALWAYS in PHP7. I ran into the same problem testing on my Dev. What I would do (since there's no failure cases here) is the following
if(gettype($row['data']) != 'string') return ''; // It's not a string so return empty for failure
return $row['data'];
Now, as to your error, your problem seems to be a bad reference. Without seeing the code that calls it, this looks like your problem
try{
$this->dbh = new PDO($dsn, $this->user, $this->pass, $options);
}
// Catch any errors
catch(PDOException $e){
$this->error = $e->getMessage();
}
So the catch block is catching any failures and setting an internal variable. Something is not checking that to make sure you have a PDO object. You might want to echo $e->getMessage();
and see if you're getting any exceptions. Because the error you posted is indicating that the above block failed.