3
\$\begingroup\$

I need you help with my php session class. I can not understand, the class implements adequate work with sessions. By adequate work, I understand the security and correctness of the methods.

<?php

namespace Core;


class Session
{
    /**
     * @var string Название сесии
     */
    private $name;
    /**
     * @var array Cookie сессии
     */
    private $cookie;
    /**
     * @var int Время жизни сессии
     */
    private $timeToLive;

    /**
     * Session constructor
     * @see http://php.net/manual/ru/session.configuration.php Настройка во время выполнения
     * @see http://php.net/manual/ru/function.session-set-cookie-params.php PHP session_set_cookie_params
     * @param int $time_to_live Время жизни сессии (в минутах)
     * @param string $name
     * @param array $cookie
     */
    public function __construct($time_to_live = 30, $name = "application.session", $cookie = [])
    {
        $this->timeToLive = $time_to_live;
        // Изменяется имя сеанса (по умолчанию) на указанное (если есть) имя для конкретного приложения
        $this->name = $name;
        $this->cookie = $cookie;
        // session.cookie_path определяет устанавливаемый путь в сессионной cookie
        // session.cookie_domain определяет устанавливаемый домен в сессионной cookie
        $this->cookie += [
            'lifetime' => 0,
            'path' => ini_get('session.cookie_path'),
            'domain' => ini_get('session.cookie_domain'),
            'secure' => isset($_SERVER['HTTPS']),
            'httponly' => true
        ];

        /*
         * Указывается, что сеансы должны передаваться только с помощью файлов cookie,
         * исключая возможность отправки идентификатора сеанса в качестве параметра «GET».
         * Установка параметров cookie идентификатора сеанса. Эти параметры могут быть переопределены при инициализации
         * обработчика сеанса, однако рекомендуется использовать значения по умолчанию, разрешающие отправку
         * только по HTTPS (если имеется) и ограниченный доступ HTTP (без доступа к сценарию на стороне клиента).
         */

        // Определяет, будет ли модуль использовать cookies для хранения идентификатора сессии на стороне клиента
        ini_set('session.use_cookies', 1);
        // Определяет, будет ли модуль использовать только cookies для хранения идентификатора сессии на стороне клиента
        ini_set('session.use_only_cookies', 1);
        session_set_cookie_params(
            $this->cookie['lifetime'],
            $this->cookie['path'],
            $this->cookie['domain'],
            $this->cookie['secure'],
            $this->cookie['httponly']
        );
    }

    public function __get($name)
    {
        switch ($name) {
            case 'isActive':
                return $this->getActive();
            case 'id':
                return $this->getId();
            case 'name':
                return isset($this->name) ? $this->name : $this->getName();
            case 'isValid':
                return $this->isValid();
        }
    }

    public function __set($name, $value)
    {
        switch ($name) {
            case 'id':
                $this->setId($value);
                break;
            case 'name':
                $this->setName($value);
                break;
            case 'timeToLive':
                $this->timeToLive = $value * 60;
                break;
        }
    }

    /**
     * Получение статуса активности сессии
     * @see https://secure.php.net/manual/en/function.session-status.php PHP session_status
     * @return bool Статус активности сессии
     */
    private function getActive()
    {
        return session_status() === PHP_SESSION_ACTIVE;
    }

    /**
     * Получение идентификатора текущей сессии.
     * Метод является оберткой для реализации стандартного метода.
     * @see https://secure.php.net/manual/ru/function.session-id.php PHP session_id
     * @return string
     */
    private function getId()
    {
        return session_id();
    }

    /**
     * Получение имени сессии
     * Метод является оберткой для реализации стандартного метода
     * @see http://php.net/manual/ru/function.session-name.php PHP session_name
     * @return string|null
     */
    private function getName()
    {
        return $this->isActive ? session_name() : null;
    }

    private function isValid()
    {
        return !$this->isExpired() && $this->isFingerprint();
    }

    /**
     * Проверка срока действия сессии
     * @return bool
     */
    private function isExpired()
    {
        $activity = isset($_SESSION['_last_activity']) ? $_SESSION['_last_activity'] : false;
        if ($activity && ((time() - $activity) > $this->timeToLive)) {
            return true;
        }
        $_SESSION['_last_activity'] = time();

        return false;
    }

    /**
     * Проверка клиента
     * @return bool
     */
    private function isFingerprint()
    {
        $hash = sha1($_SERVER['HTTP_USER_AGENT'] .
            (ip2long($_SERVER['REMOTE_ADDR']) & ip2long('255.255.0.0')));

        if (isset($_SESSION['_fingerprint'])) {
            return $_SESSION['_fingerprint'] === $hash;
        }

        $_SESSION['_fingerprint'] = $hash;

        return true;
    }

    /**
     * Назначение идентификатора текущей сессии.
     * Метод является оберткой для реализации стандартного метода.
     * @see https://secure.php.net/manual/ru/function.session-id.php PHP session_id
     * @param string $id Идентификатор сессии для текущей сессии
     */
    private function setId($id)
    {
        session_id($id);
    }

    /**
     * Установка имени сессии
     * Метод является оберткой для реализации стандартного метода
     * @see http://php.net/manual/ru/function.session-name.php PHP session_name
     * @param $name
     */
    public function setName($name)
    {
        if ($this->isActive) {
            session_name($name);
        }
    }

    /**
     * Инициализация сессии
     */
    public function open()
    {
        // Бездействие, если сессия была инициализирована ранее
        if ($this->isActive) {
            return;
        }

        session_start();
        // Проверка на корректность инициализированнйо сессии
        if (!$this->isActive) {
            // TODO: Вывод исключения
        }
    }

    /**
     * Уничтожение сессии, включая все атрибуты. Метод имеет эффект только при наличии активной сессии.
     * @see http://php.net/manual/ru/function.setcookie.php PHP setcookie
     */
    public function destroy()
    {
        if ($this->isActive) {
            $this->deleteAll();
            setcookie(
                $this->name,
                time() - 42000,
                $this->cookie['path'],
                $this->cookie['domain'],
                $this->cookie['secure'],
                $this->cookie['httponly']
            );

            session_destroy();
        }
    }

    /**
     * Удаление всех значений сессии
     * Метод является оберткой для реализации стандартного метода
     * @see http://php.net/manual/ru/function.session-unset.php PHP session_unset
     */
    public function deleteAll()
    {
        if ($this->isActive) {
            session_unset();
        }
    }

    /**
     * Обновление текущего ID на новый. Метод имеет эффект только при наличии активной сессии.
     * @see https://secure.php.net/session_regenerate_id PHP session_regenerate_id
     * @param bool $delete_old_session
     */
    public function refresh($delete_old_session = true)
    {
        if ($this->isActive) {
            session_regenerate_id($delete_old_session);
        }
    }

    /**
     * Получение значение сессии по ключу.
     * @param string $key Ключ, по которому необходимо получить значения
     * @return null|mixed Значение сессии по ключу
     */
    public function get($key)
    {
        if ($this->isActive) {
            return isset($_SESSION[$key]) ? $_SESSION[$key] : null;
        }

        return null;
    }

    /**
     * Добавление или установка значений в сессию по ключу
     * @param string $key Ключ, в который необходимо добавить значения
     * @param string $value Значение добавления
     */
    public function set($key, $value)
    {
        if ($this->isActive) {
            $_SESSION[$key] = $value;
        }
    }

    /**
     * Удаление значения сессии по ключу
     * @param string $key Ключ, по которому необходимо удалить значения
     */
    public function delete($key)
    {
        if ($this->isActive && isset($_SESSION[$key])) {
            unset($_SESSION[$key]);
        }
    }

    /**
     * Проверка наличия ключа у сессии
     * @param string $key Ключ, в который необходимо найти
     * @return bool
     */
    public function hasKey($key)
    {
        return ($this->isActive && isset($_SESSION[$key]));
    }
}

Usage

$session = new Session();
$session->open();
// If AFK more than access - logout
if (!$session->isValid) {
    $session->destroy();
}
...

What are your thoughts and comments on this decision?

\$\endgroup\$
  • \$\begingroup\$ Welcome to Code Review! The (doc) comments would be of little value for me but for web translators. \$\endgroup\$ – greybeard Mar 21 at 16:03
  • \$\begingroup\$ @greybeard ok, I will translate it \$\endgroup\$ – mepihindeveloper Mar 21 at 17:10
1
\$\begingroup\$

Securing PHP sessions is like art and the defualt session handler is not safe far from it and alot can go wrong with it..

For example on shared webhosting when the sessions are configured to run in one directory.
A attacker could run the PHP code on his hosting account

<?php 

session_start();
$session_id = session_id(); 

$path = session_save_path() . '/sess_' . session_id();
chmod($path, 777); # read and writeable by everybody

var_dump($session_id);

?>

The attacker can now use the session id and change the HTTP_REFERER in a HTTP header tool, to use his session_id on your website.

That makes it also possible read and modify session data on his web hosting account to gain more privilies or do SQL injections on your web hosting account.

This works because the session source code in session_start() does not check which sessions belongs to which website, the only check is HTTP_REFERER header which can be spoofed.

/* Check whether the current request was referred to by
 * an external site which invalidates the previously found id. */

if (PS(id) &&
        PS(extern_referer_chk)[0] != '\0' &&
        PG(http_globals)[TRACK_VARS_SERVER] &&
        zend_hash_find(Z_ARRVAL_P(PG(http_globals)[TRACK_VARS_SERVER]), "HTTP_REFERER", sizeof("HTTP_REFERER"), (void **) &data) == SUCCESS &&
        Z_TYPE_PP(data) == IS_STRING &&
        Z_STRLEN_PP(data) != 0 &&
        strstr(Z_STRVAL_PP(data), PS(extern_referer_chk)) == NULL
) {
    efree(PS(id));
    PS(id) = NULL;
    PS(send_cookie) = 1;
    if (PS(use_trans_sid) && !PS(use_only_cookies)) {
        PS(apply_trans_sid) = 1;
    }
}

How to make it safe?
You have two good options

1) Use session_save_path
2) Write you own SessionHandler class and write your own layer to work on file system and or database.

Note
This will not solve all session related security issues when using PHP sessions, but it is a good starting point.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.