За последние 24 часа нас посетили 21808 программистов и 1070 роботов. Сейчас ищут 620 программистов ...

Безопасный класс работы с сессиями.

Тема в разделе "PHP для профи", создана пользователем mepihindeveloper, 21 мар 2019.

  1. mepihindeveloper

    mepihindeveloper Активный пользователь

    С нами с:
    20 ноя 2018
    Сообщения:
    12
    Симпатии:
    1
    Здравствуйте. У меня есть вопрос по работе с сессиями. Я реализовал класс для работы с сессиями, но точно не могу определить его адекватность и корректность.
    PHP:
    1. <?php
    2.  
    3. namespace Core;
    4.  
    5.  
    6. class Session
    7. {
    8.     /**
    9.      * @var string Название сесии
    10.      */
    11.     private $name;
    12.     /**
    13.      * @var array Cookie сессии
    14.      */
    15.     private $cookie;
    16.     /**
    17.      * @var int Время жизни сессии
    18.      */
    19.     private $timeToLive;
    20.  
    21.     /**
    22.      * Session constructor
    23.      * @see https://php.ru/manual/session.configuration.html Настройка во время выполнения
    24.      * @see https://php.ru/manual/function.session-set-cookie-params.html PHP session_set_cookie_params
    25.      * @param int $time_to_live Время жизни сессии (в минутах)
    26.      * @param string $name
    27.      * @param array $cookie
    28.      */
    29.     public function __construct($time_to_live = 30, $name = "application.session", $cookie = [])
    30.     {
    31.         $this->timeToLive = $time_to_live;
    32.         // Изменяется имя сеанса (по умолчанию) на указанное (если есть) имя для конкретного приложения
    33.         $this->name = $name;
    34.         $this->cookie = $cookie;
    35.         // session.cookie_path определяет устанавливаемый путь в сессионной cookie
    36.         // session.cookie_domain определяет устанавливаемый домен в сессионной cookie
    37.         $this->cookie += [
    38.             'lifetime' => 0,
    39.             'path' => ini_get('session.cookie_path'),
    40.             'domain' => ini_get('session.cookie_domain'),
    41.             'secure' => isset($_SERVER['HTTPS']),
    42.             'httponly' => true
    43.         ];
    44.  
    45.         /*
    46.          * Указывается, что сеансы должны передаваться только с помощью файлов cookie,
    47.          * исключая возможность отправки идентификатора сеанса в качестве параметра «GET».
    48.          * Установка параметров cookie идентификатора сеанса. Эти параметры могут быть переопределены при инициализации
    49.          * обработчика сеанса, однако рекомендуется использовать значения по умолчанию, разрешающие отправку
    50.          * только по HTTPS (если имеется) и ограниченный доступ HTTP (без доступа к сценарию на стороне клиента).
    51.          */
    52.  
    53.         // Определяет, будет ли модуль использовать cookies для хранения идентификатора сессии на стороне клиента
    54.         ini_set('session.use_cookies', 1);
    55.         // Определяет, будет ли модуль использовать только cookies для хранения идентификатора сессии на стороне клиента
    56.         ini_set('session.use_only_cookies', 1);
    57.             $this->cookie['lifetime'],
    58.             $this->cookie['path'],
    59.             $this->cookie['domain'],
    60.             $this->cookie['secure'],
    61.             $this->cookie['httponly']
    62.         );
    63.     }
    64.  
    65.     public function __get($name)
    66.     {
    67.         switch ($name) {
    68.             case 'isActive':
    69.                 return $this->getActive();
    70.             case 'id':
    71.                 return $this->getId();
    72.             case 'name':
    73.                 return isset($this->name) ? $this->name : $this->getName();
    74.             case 'isValid':
    75.                 return $this->isValid();
    76.         }
    77.     }
    78.  
    79.     public function __set($name, $value)
    80.     {
    81.         switch ($name) {
    82.             case 'id':
    83.                 $this->setId($value);
    84.                 break;
    85.             case 'name':
    86.                 $this->setName($value);
    87.                 break;
    88.             case 'timeToLive':
    89.                 $this->timeToLive = $value * 60;
    90.                 break;
    91.         }
    92.     }
    93.  
    94.     /**
    95.      * Получение статуса активности сессии
    96.      * @see https://secure.php.net/manual/en/function.session-status.php PHP session_status
    97.      * @return bool Статус активности сессии
    98.      */
    99.     private function getActive()
    100.     {
    101.         return session_status() === PHP_SESSION_ACTIVE;
    102.     }
    103.  
    104.     /**
    105.      * Получение идентификатора текущей сессии.
    106.      * Метод является оберткой для реализации стандартного метода.
    107.      * @see https://secure.php.net/manual/ru/function.session-id.php PHP session_id
    108.      * @return string
    109.      */
    110.     private function getId()
    111.     {
    112.         return session_id();
    113.     }
    114.  
    115.     /**
    116.      * Получение имени сессии
    117.      * Метод является оберткой для реализации стандартного метода
    118.      * @see https://php.ru/manual/function.session-name.html PHP session_name
    119.      * @return string|null
    120.      */
    121.     private function getName()
    122.     {
    123.         return $this->isActive ? session_name() : null;
    124.     }
    125.  
    126.     private function isValid()
    127.     {
    128.         return !$this->isExpired() && $this->isFingerprint();
    129.     }
    130.  
    131.     /**
    132.      * Проверка срока действия сессии
    133.      * @return bool
    134.      */
    135.     private function isExpired()
    136.     {
    137.         $activity = isset($_SESSION['_last_activity']) ? $_SESSION['_last_activity'] : false;
    138.         if ($activity && ((time() - $activity) > $this->timeToLive)) {
    139.             return true;
    140.         }
    141.         $_SESSION['_last_activity'] = time();
    142.  
    143.         return false;
    144.     }
    145.  
    146.     /**
    147.      * Проверка клиента
    148.      * @return bool
    149.      */
    150.     private function isFingerprint()
    151.     {
    152.         $hash = sha1($_SERVER['HTTP_USER_AGENT'] .
    153.             (ip2long($_SERVER['REMOTE_ADDR']) & ip2long('255.255.0.0')));
    154.  
    155.         if (isset($_SESSION['_fingerprint'])) {
    156.             return $_SESSION['_fingerprint'] === $hash;
    157.         }
    158.  
    159.         $_SESSION['_fingerprint'] = $hash;
    160.  
    161.         return true;
    162.     }
    163.  
    164.     /**
    165.      * Назначение идентификатора текущей сессии.
    166.      * Метод является оберткой для реализации стандартного метода.
    167.      * @see https://secure.php.net/manual/ru/function.session-id.php PHP session_id
    168.      * @param string $id Идентификатор сессии для текущей сессии
    169.      */
    170.     private function setId($id)
    171.     {
    172.         session_id($id);
    173.     }
    174.  
    175.     /**
    176.      * Установка имени сессии
    177.      * Метод является оберткой для реализации стандартного метода
    178.      * @see https://php.ru/manual/function.session-name.html PHP session_name
    179.      * @param $name
    180.      */
    181.     public function setName($name)
    182.     {
    183.         if ($this->isActive) {
    184.             session_name($name);
    185.         }
    186.     }
    187.  
    188.     /**
    189.      * Инициализация сессии
    190.      */
    191.     public function open()
    192.     {
    193.         // Бездействие, если сессия была инициализирована ранее
    194.         if ($this->isActive) {
    195.             return;
    196.         }
    197.  
    198.         session_start();
    199.         // Проверка на корректность инициализированнйо сессии
    200.         if (!$this->isActive) {
    201.             // TODO: Вывод исключения
    202.         }
    203.     }
    204.  
    205.     /**
    206.      * Уничтожение сессии, включая все атрибуты. Метод имеет эффект только при наличии активной сессии.
    207.      * @see https://php.ru/manual/function.setcookie.html PHP setcookie
    208.      */
    209.     public function destroy()
    210.     {
    211.         if ($this->isActive) {
    212.             $this->deleteAll();
    213.             setcookie(
    214.                 $this->name,
    215.                 time() - 42000,
    216.                 $this->cookie['path'],
    217.                 $this->cookie['domain'],
    218.                 $this->cookie['secure'],
    219.                 $this->cookie['httponly']
    220.             );
    221.  
    222.             session_destroy();
    223.         }
    224.     }
    225.  
    226.     /**
    227.      * Удаление всех значений сессии
    228.      * Метод является оберткой для реализации стандартного метода
    229.      * @see https://php.ru/manual/function.session-unset.html PHP session_unset
    230.      */
    231.     public function deleteAll()
    232.     {
    233.         if ($this->isActive) {
    234.             session_unset();
    235.         }
    236.     }
    237.  
    238.     /**
    239.      * Обновление текущего ID на новый. Метод имеет эффект только при наличии активной сессии.
    240.      * @see https://secure.php.net/session_regenerate_id PHP session_regenerate_id
    241.      * @param bool $delete_old_session
    242.      */
    243.     public function refresh($delete_old_session = true)
    244.     {
    245.         if ($this->isActive) {
    246.             session_regenerate_id($delete_old_session);
    247.         }
    248.     }
    249.  
    250.     /**
    251.      * Получение значение сессии по ключу.
    252.      * @param string $key Ключ, по которому необходимо получить значения
    253.      * @return null|mixed Значение сессии по ключу
    254.      */
    255.     public function get($key)
    256.     {
    257.         if ($this->isActive) {
    258.             return isset($_SESSION[$key]) ? $_SESSION[$key] : null;
    259.         }
    260.  
    261.         return null;
    262.     }
    263.  
    264.     /**
    265.      * Добавление или установка значений в сессию по ключу
    266.      * @param string $key Ключ, в который необходимо добавить значения
    267.      * @param string $value Значение добавления
    268.      */
    269.     public function set($key, $value)
    270.     {
    271.         if ($this->isActive) {
    272.             $_SESSION[$key] = $value;
    273.         }
    274.     }
    275.  
    276.     /**
    277.      * Удаление значения сессии по ключу
    278.      * @param string $key Ключ, по которому необходимо удалить значения
    279.      */
    280.     public function delete($key)
    281.     {
    282.         if ($this->isActive && isset($_SESSION[$key])) {
    283.             unset($_SESSION[$key]);
    284.         }
    285.     }
    286.  
    287.     /**
    288.      * Проверка наличия ключа у сессии
    289.      * @param string $key Ключ, в который необходимо найти
    290.      * @return bool
    291.      */
    292.     public function hasKey($key)
    293.     {
    294.         return ($this->isActive && isset($_SESSION[$key]));
    295.     }
    296. }
    Использование такое:
    PHP:
    1. $session = new Session();
    2. $session->open();
    3. // If AFK more than access - logout
    4. if (!$session->isValid) {
    5.     $session->destroy();
    6. }
    7. ...
     
  2. Maputo

    Maputo Активный пользователь

    С нами с:
    30 июл 2015
    Сообщения:
    1.136
    Симпатии:
    173
    В чем же безопасность работы заключается?
    Так же метод hasKey() можно было бы использовать внутри методов get() и delete().
     
  3. mepihindeveloper

    mepihindeveloper Активный пользователь

    С нами с:
    20 ноя 2018
    Сообщения:
    12
    Симпатии:
    1
    Безопасность - корректность работы с сессией. Исключить базовые моменты взлома
     
  4. Vanchot

    Vanchot Активный пользователь

    С нами с:
    23 мар 2019
    Сообщения:
    104
    Симпатии:
    19
    Адрес:
    Ахерон (LV-426)
    а... зачем это всё? :)
     
  5. artoodetoo

    artoodetoo Суперстар
    Команда форума Модератор

    С нами с:
    11 июн 2010
    Сообщения:
    11.072
    Симпатии:
    1.237
    Адрес:
    там-сям
    @mepihindeveloper продемонстрируй вектор атаки, от которого, по твоему мнению, защищает этот класс.

    Мне непонятно. Буду рад узнать что-то новое, тем более, что ты поместил тему в раздел Профи. Наверное ты что-то такое знаешь...
     
  6. Павел Голубцов

    Павел Голубцов Активный пользователь

    С нами с:
    4 мар 2019
    Сообщения:
    183
    Симпатии:
    4
    Отошел от компа, а кто то подойдет после 42000 11.6ч и не воспользуется твоей сессий если я все правильно понял.
     
  7. artoodetoo

    artoodetoo Суперстар
    Команда форума Модератор

    С нами с:
    11 июн 2010
    Сообщения:
    11.072
    Симпатии:
    1.237
    Адрес:
    там-сям
    Почитал код, нормально в принципе. Правда смена IP у пользователя это не такое уж редкое явление. Фингерпринт можно подшаманить под свои вкусы, в принципе. :) Я бы IP убрал, а добавил ещё пару заголовков запроса.
     
  8. [vs]

    [vs] Суперстар
    Команда форума Модератор

    С нами с:
    27 сен 2007
    Сообщения:
    10.553
    Симпатии:
    631
    IP даже банки не чекают. Кто-то например полтора часа до работы едет, так IP десять раз сменится.
    --- Добавлено ---
    HTTPS в принципе сделал куки безопасными, даже фингерпринт это экстра, хотя имеет смысл при атаке непосредственно на устройство пользователя
     
  9. mepihindeveloper

    mepihindeveloper Активный пользователь

    С нами с:
    20 ноя 2018
    Сообщения:
    12
    Симпатии:
    1
    Согласен про IP, вопрос к вам про заголовки: "что бы вы передавали?"
     
  10. artoodetoo

    artoodetoo Суперстар
    Команда форума Модератор

    С нами с:
    11 июн 2010
    Сообщения:
    11.072
    Симпатии:
    1.237
    Адрес:
    там-сям
    Просто загляни в инспектор браузера, вкладка Network. Возьми любой запрос и посмотри его Request headers. Как минимум, всё, что начинается на "Accept" можно включить в отпечаток.
    --- Добавлено ---
    Про фингерпринтинг и идентификацию клиента много пишут. Это выйдет за рамки твоей текущей задачи ;)
    Я вообще не уверен надо ли с т.з. ООП включать уловки по идентификации клиента в класс Сессия.
     
  11. mepihindeveloper

    mepihindeveloper Активный пользователь

    С нами с:
    20 ноя 2018
    Сообщения:
    12
    Симпатии:
    1
    Реализовал систему и обнаружил, что заголовок http_accept изменчив. Получилось, что сначала он запрашивает картинку (фавикон), а потом страницы и эти 2 заголовка разные. Есть ли возможность как-то настроить эту вещь? Ну, скажем, какое-то условие, которое игнорировало бы http_accept, если обращение к картинкам. То есть, создавать и работать с сессиями только если запросы к страницам.
     
  12. artoodetoo

    artoodetoo Суперстар
    Команда форума Модератор

    С нами с:
    11 июн 2010
    Сообщения:
    11.072
    Симпатии:
    1.237
    Адрес:
    там-сям
    Ну вот, для меня это выглядит как ещё один аргумент в пользу того, что класс сессии не должен отвечать за такие вещи. Он находится на более низком уровне чем контроль доступа.

    Если ты знаком с Laravel, то знаешь, что каждому маршруту (или группе маршрутов) можно сопоставить middleware. Таким образом можно контролировать доступ к разделу /admin/, например, но ничего не предпринимать при доступе к другим адресам. Так вот, логично поместить контроль за фингерпринтом в этот самый мидлвар. А не в класс сессии. Мидлвар решает надо ли открывать сессию, надо ли идентифицировать пользователя и как это делать.
     
  13. sobachnik

    sobachnik Старожил

    С нами с:
    20 апр 2007
    Сообщения:
    3.380
    Симпатии:
    13
    Адрес:
    Дмитров, МО
    А мне, если честно, не понятно, зачем мне печатать так много кода для того, чтобы начать сессию?
    Мне кажется, должно быть достаточно одной строки $session = new Session();
    Всё остальное можно уже внутри сделать.
    Если хочется дать возможность выбора - удалять или нет сессию, которая !isValid - можно параметр в конструкторе добавить для этого со значением по-умолчанию...
     
  14. artoodetoo

    artoodetoo Суперстар
    Команда форума Модератор

    С нами с:
    11 июн 2010
    Сообщения:
    11.072
    Симпатии:
    1.237
    Адрес:
    там-сям
    „SRP (принцип единственной ответственности) в ООП обозначает, что на каждый класс нужно возложить только одну определённую ответственность, и его поведения должны быть направлены исключительно на обеспечение этой ответственности.“

    — из описания SOLID Principles
     
  15. artoodetoo

    artoodetoo Суперстар
    Команда форума Модератор

    С нами с:
    11 июн 2010
    Сообщения:
    11.072
    Симпатии:
    1.237
    Адрес:
    там-сям
    Это была реакция на "всё остальное внутри".
    ПМСМ, класс "Сессия" сам не должен принимать решений. Он обеспечивает уровень хранения и только. А выше может быть класс "Защитник" или типа того, из которого мы можем запросить объект "Идентифицированный пользователь", например.
     
  16. sobachnik

    sobachnik Старожил

    С нами с:
    20 апр 2007
    Сообщения:
    3.380
    Симпатии:
    13
    Адрес:
    Дмитров, МО
    Ну он и не будет принимать решений, он будет просто выполнять. А решение принимает тот, кто передаёт или не передаёт аргумент в конструктор
     
  17. DarkU

    DarkU Активный пользователь

    С нами с:
    15 июл 2013
    Сообщения:
    12
    Симпатии:
    2
    Просто не ефективно. Это примерно так: === ко всему и в 80% случаев ваша сессия буде отваливаться. (обновил браузер, изменился IP и т.д.)
    Также
    очень забавные куки.

    Реализалия всего "в одном месте" как минимум головная боль следующему программисту или вас ще, через 6 месяцев не использование этого скрипта.
    Вложите клас создания сессий, класс для создание cockie и клас который все это проверяет на "беопасность". + если это "смертельно важное место" - логирование всего что происходить с сохранение в БД и/или в файл на сервере если БД упала.
     
  18. mike4ip

    mike4ip Новичок

    С нами с:
    24 авг 2019
    Сообщения:
    18
    Симпатии:
    1
    В случае с сессиями вообще очень трудно накосячить, гораздо более слабое место - SQL-инъекции и XSS.

    Вообще в PHP-сессиях мне больше всего не нравится лёгкость подмены, но в вашем случае эта дыра залатана, так что не знаю, в чём упрекнуть.