Правда о Сессиях
Почти каждое PHP-приложение использует сессии. Эта статья детально
рассматривает реализацию безопасного механизма управления сессией на PHP. После
базового введения в архитектуру, лежащую в основе web'а, после знакомства с
проблемой сохранения состояния, с основным применением и смыслом кук, я покажу
вам несколько простых и эффективных методов, которые могут быть использованы для
увеличения безопасности и надёжности PHP-приложений, нуждающихся в сохранении
состояния.
Распространено неправильное представление, что PHP обеспечивает определённый
уровень безопасности с помощью собственных возможностей управления сессией. PHP
же, напротив, просто предоставляет удобный, подходящий механизм, а обеспечение
законченного решения является заботой разработчика; и, как вы увидите, не
существует единого метода решения, наилучшего для всех.
Невозможность сохранения состояния
Протокол передачи гипертекста (HTTP), являющийся движущей силой web'а, -
это протокол без сохранения состояния. Связано это с тем, что в нём нет
ничего, что требует от браузера идентифицировать себя при каждом запросе, а
также отсутствует постоянно установленное соединение между браузером и
web-сервером, которое сохранялось бы от страницы к странице. Когда
пользователь посещает web-сайт, браузер посылает HTTP-запрос
web-серверу, который в свою очередь возвращает HTTP-ответ. Этим
взаимодействие и ограничивается, это и представляет собой завершённую
HTTP-транзакцию.
Так как обмен информацией в web'е основан на HTTP, сохранение состояния в
web-приложении может быть для разработчиков очень сложной задачей.
Куки - это расширение протокола HTTP, которое было введено, чтобы помочь в
обеспечении сохраняющих состояние HTTP-транзакций, но забота о
конфиденциальности побуждает многих пользователей отключать поддержку кук.
Информация о состоянии может быть передана через URL, однако её случайное
раскрытие представляет собой серьёзную угрозу безопасности. Фактически, сама
суть сохранения состояния требует, чтобы клиент идентифицировал себя, однако те
из нас, кто задумывается о безопасности, знают, что мы никогда не должны
доверять информации, посланной клиентом.
Несмотря на всё это, существуют изящные способы решения проблемы сохранения
состояния. Конечно, идеального решения не существует, как и способа, который
может удовлетворить нужды каждого. Эта статья представляет некоторые способы,
которые могут надёжно обеспечить возможность сохранения состояния, а также
защитить от основанных на сессии атак, таких как ВСД {Б.С.: в
оригинале - impersonation.} - выдача себя за
другого (подделка сессии). Попутно вы изучите, как на самом деле работают
куки, что делают PHP сессии и что требуется для подделки сессии.
Общее представление об HTTP
Для того чтобы хорошо понять проблему сохранения состояния, а также выбрать
наилучшее для ваших нужд решение, важно немного разобраться в лежащей в основе
web'а архитектуре - протоколе передачи гипертекста (HTTP).
Визит на http://www.example.org/ требует от
web-браузера послать HTTP-запрос к www.example.org на порт
80. Синтаксис запроса подобен следующему:
GET /
HTTP/1.1 Host: www.example.org
Первая строка называется строкой-запросом, а второй параметр в ней (слеш в
нашем примере) - это путь к запрашиваемому ресурсу. Слеш обозначает корень
дерева документов; web-сервер преобразовывает его в конкретный путь
в файловой системе. Пользователи Apache'а могут быть знакомы с указанием этого
пути директивой DocumentRoot. Если запрашивается
http://www.example.org/path/to/script.php, то заданным в запросе путём
к ресурсу является /path/to/script.php. Если корень дерева документов
определён как /usr/local/apache/htdocs, то полный путь к ресурсу,
используемый web-сервером, -
/usr/local/apache/htdocs/path/to/script.php.
Вторая строка иллюстрирует синтаксис HTTP-заголовка. В нашем случае это
заголовок Host; он определяет доменное имя хоста, с которого браузер
запрашивает ресурс. Этот заголовок согласно протоколу HTTP/1.1 является
обязательным и помогает обеспечивать механизм поддержки виртуального
хостинга - множества доменов, обслуживаемых с единственного IP-адреса
(часто - единственным сервером). Существует много других (необязательных)
заголовков, которые могут быть включены в запрос; вам может быть знакомо
обращением к ним в вашем PHP-коде, например,
$_SERVER['HTTP_REFERER'] и
$_SERVER['HTTP_USER_AGENT'].
Обратите особое внимание, что в запросе-примере нет ничего, что может
использоваться для однозначного распознавания клиента. Некоторые разработчики
прибегают к информации, полученной от TCP/IP (такой, например, как IP-адрес) для
уникальной идентификации, но этот подход сопряжён с множеством трудностей.
Наибольшая из них - один пользователь потенциально может использовать
различные IP-адреса для каждого запроса (как это происходит с пользователями
AOLа), а разные пользователи могут использовать одни и те же IP-адреса (как в
случае использования во многих компьютерных лабораториях HTTP-прокси). Эти
ситуации могут послужить причиной того, что единственный пользователь может
выглядеть как множество, или множество пользователей - как один. Для
сколько-нибудь надёжного и безопасного метода обеспечения состояния может
использоваться только информация, полученная из HTTP.
Первый шаг в задаче сохранения состояния - как-то однозначно
идентифицировать каждого клиента. Так как единственная надёжная информация,
которая может быть для этого использована, должна приходить из HTTP-запроса,
что-то должно быть в самом запросе, что может использоваться для такой
идентификации. Есть несколько путей реализации этого, но решением, разработанным
именно для этой проблемы, являются куки.
Куки
Понимание того, что должен существовать метод однозначной идентификации
клиентов, привёл к творческому решению проблемы - созданию кук. Куки
легче всего понять, если вы примете во внимание, что они являются расширением
HTTP-протокола, который и определяет точно, что же это такое. Куки определяются
в RFC 2965, хотя первоначальная спецификация, написанная в Netscape wp.netscape.com/newsref/std/cookie_spec.html, более близка к
поддерживаемой индустрией.
Существует два HTTP-заголовка, которые необходимы для реализации механизма
кук, - Set-Cookie и Cookie.
Web-сервер включает заголовок Set-Cookie в
ответ на запрос для того, чтобы браузер включал эту куку в последующие запросы.
Браузер, в котором разрешены куки, включает заголовок Cookie во все
последующие запросы (которые удовлетворяют условиям, определённым в заголовке
Set-Cookie), пока кука не устареет (пока не истечёт её
срок). Типичный сценарий состоит из двух транзакций (четырёх HTTP-сообщений):
- Клиент шлёт HTTP-запрос.
- Сервер шлёт HTTP-ответ, включающий заголовок
Set-Cookie.
- Клиент шлёт HTTP-запрос, включающий заголовок Cookie.
- Сервер шлёт HTTP-ответ.
Добавление заголовка Cookie во
втором запросе клиента (пункт 3) предоставляет информацию, которую сервер может
использовать для однозначной идентификации клиента. Так же в этот момент сервер
(или PHP-скрипт на стороне сервера) может определить, разрешены ли у
пользователя куки. Хотя пользователь может предпочесть запрет кук, достаточно
безопасно будет предположить, что настройки пользователя не изменяются во время
взаимодействия с вашим приложением. Этот факт, как вскоре будет
продемонстрировано, может оказаться очень полезным.
GET- и POST-данные
Существует ещё два метода, которые клиент может использовать для отсылки
данных на сервер, и эти методы существуют гораздо дольше кук. Клиент может
поместить информацию в запрашиваемом URLе в строке запроса или прямо в пути,
хотя последний случай требует специфического программирования, которое не
освещено в этой статье. Как пример использования строки запроса рассмотрим
следующий:
GET /index.php?foo=bar HTTP/1.1 Host:
www.example.org
Принимающий скрипт index.php может обратиться к
$_GET['foo'] для получения значения параметра foo. Вследствие
этого, большинство PHP-разработчиков говорят об этих данных как о
GET-данных (другие иногда ссылаются на них как на данные из запроса или
URL-переменные). Распространённой причиной путаницы является то, что GET-данные
могут существовать и в POST-запросе, так как они являются просто частью
запрашиваемого URLя и не зависят от метода запроса.
Другим методом, которым клиент может воспользоваться для отсылки информации,
является использование содержимого HTTP-запроса. Этот способ требует, чтобы
методом запроса являлся POST:
POST /index.php
HTTP/1.1 Host: www.example.org Content-Type:
application/x-www-form-urlencoded Content-Length:
7 foo=bar
В этом случае принимающий скрипт index.php может обращаться к
$_POST['foo'] для получения значения параметра foo.
PHP-разработчики обычно ссылаются на эти данные как на POST-данные, и
именно так браузер передаёт данные, отсылаемые формой, в которой указано
method="post".
Вообще же говоря, запрос может содержать и оба типа данных:
POST
/index.php?getvar=foo HTTP/1.1 Host:
www.example.org Content-Type:
application/x-www-form-urlencoded Content-Length:
11 postvar=bar
Эти два дополнительных метода отправки данных в запросе могут обеспечить
замену кукам. В отличие от кук, поддержка GET- и POST-данных не является
опциональной, поэтому эти методы могут также быть более надёжными. Рассмотрим
уникальный идентификатор, называемый PHPSESSID {Б.С. начинающим:
вообще, это имя-по-умолчанию может быть изменено разработчиком PHP-приложения на
другое.}, включённый в запрашиваемый URL:
GET
/index.php?PHPSESSID=12345 HTTP/1.1 Host: www.example.org
{Б.С. начинающим: в действительности встроенный PHP-шный механизм
управления сессией генерирует более длинные, чем 12345, идентификаторы
сессии.}
Этим достигается та же цель, что и заголовком Cookie, так как клиент
идентифицирует себя; однако такой способ требует гораздо большего участия
разработчика. Как только кука установлена, - обязанностью браузера является
возвращать её в последующих запросах. Для передачи же уникального идентификатора
посредством URLя разработчик должен обеспечить, чтобы все ссылки, кнопки
отправки форм и т.п. содержали соответствующую строку запроса (впрочем, PHP
может ему в этом помочь, если вы включите директиву
session.use_trans_sid). К тому же, GET-данные отображаются в URLе и
гораздо уязвимее, чем куки. Фактически, ничего не подозревающие пользователи
могут сохранить такой URL в закладках, послать его другу или сделать с ним что
угодно, что может случайно раскрыть уникальный идентификатор.
Хотя POST-данные могут быть раскрыты с меньшей вероятностью, передача
уникального идентификатора как POST-переменной требует, чтобы все запросы
пользователя осуществлялись методом POST. Обычно этот вариант не удобен, хотя
дизайн вашего приложения и может сделать его более жизнеспособным.
Управление сессией
До сих пор я обсуждал состояние - несколько низкоуровневое понятие,
касающееся привязки одной HTTP-транзакции к другой. Более полезной возможностью,
с которой вы, вероятно, знакомы, является управление сессией. Управление
сессией не только основывается на возможности сохранения состояния, но также
нуждается в сохранении клиентских данных для каждой пользовательской сессии. Эти
данные в большинстве случаев называют сессионными данными, потому что они
связаны с конкретной сессией пользователя. Если вы используете встроенный в PHP
механизм управления сессией, сессионные данные сохраняются для вас (по
умолчанию - в директории /tmp) и доступны в суперглобальном
массиве $_SESSION. Простой пример использования сессий касается
сохранения сессионных данных от одной страницы к другой. Листинг 1,
представляющий скрипт session_start.php, показывает, как это может быть
сделано.
Листинг 1
<?php
session_start();
$_SESSION['foo'] =
'bar';
<a href="session_continue.php">session_continue.php</a>
Если пользователь кликнет по ссылке в session_start.php {Б.С.
начинающим: конечно, имеется в виду по ссылке на сформированной этим скриптом
странице.}, принимающий скрипт session_continue.php получит
возможность доступа к той же сессионной переменной $_SESSION['foo'].
Это показано в листинге 2.
Листинг 2
<?php
session_start();
echo $_SESSION['foo']; /*
bar */
{Б.С. начинающим: чтобы произошло вышеописанное, второй скрипт должен
получить идентификатор сессии, сгенерированный первым скриптом, для чего должна
быть включена директива session.use_trans_sid или у пользователя должны
быть разрешены куки.}
Серьёзная угроза безопасности существует, когда вы пишите код, похожий на
приведённый, без понимания того, что для вас делает PHP. Без этих знаний вы
обнаружите, что трудно отлаживать сессионные ошибки или обеспечивать какой бы то
ни было уровень безопасности.
Выдача себя за другого (ВСД)
Распространено неправильное представление, что родной PHP-шный механизм
управления сессией сам предпринимает меры безопасности против атак, основанных
на сессии. Наоборот, PHP только предоставляет подходящий для этого механизм.
Ответственность же за обеспечение соответствующих мер безопасности ложится на
разработчика. Как упоминалось ранее, не существует безупречного, идеального
способа решения, как и наилучшего решения, правильного для каждого.
Для объяснения опасности ВСД рассмотрим следующую последовательность событий:
- Мальчиш-Кибальчиш заходит на http://www.example.org/ и
авторизируется.
- Web-сайт устанавливает куку PHPSESSID=12345.
- Мальчиш-Плохиш заходит на http://www.example.org/ и предоставляет
куку PHPSESSID=12345.
- Web-сайт ошибочно думает, что Мальчиш-Плохиш в
действительности является Мальчишом-Кибальчишом.
Конечно, этот сценарий требует, чтобы
Плохиш каким-либо образом узнал или угадал правильный PHPSESSID,
принадлежащий Кибальчишу. Хоть это и кажется малообещающим, - это пример
обеспечения безопасности с помощью НДП {Б.С.: в оригинале -
obscurity.} - неизвестности для постороннего; он не
является чем-то, на что стоит полагаться. Такая неизвестность - вещь,
конечно, неплохая, и она может помогать, но необходимо что-то более
существенное, что предложит надёжную защиту против подобной
атаки.
Предотвращение ВСД
Существует много способов, которые могут быть использованы для затруднения
ВСД или других основанных на сессии атак. Общим подходом здесь является создание
систем настолько, насколько это возможно, удобными для ваших законных
пользователей и, настолько, насколько возможно, - сложными и запутанными
для атакующих. Это может оказаться очень труднодостижимым балансом, и в
значительной степени идеальный баланс зависит от дизайна приложения. Поэтому, в
конечном счёте, лучший судья - это вы сами.
Простейший корректный с точки зрения HTTP/1.1 запрос, как упоминалось ранее,
состоит из строки-запроса и заголовка Host:
GET /
HTTP/1.1 Host: www.example.org
Клиент может передать идентификатор сессии PHPSESSID в заголовке
Cookie:
GET / HTTP/1.1 Host:
www.example.org Cookie: PHPSESSID=12345
В качестве альтернативы клиент может передать идентификатор сессии в URLе
запроса:
GET /?PHPSESSID=12345 HTTP/1.1 Host:
www.example.org
Идентификатор сессии также может быть включён в POST-данные, но обычно это
менее удобно для пользователя и распространено меньше всего.
Так как информация, полученная от TCP/IP, не может быть надёжно использована
в усилении безопасности механизма, то кажется, что web-разработчик мало что
может сделать для затруднения ВСД. Как-никак, атакующий должен лишь предоставить
тот же уникальный идентификатор, что и легальный, законный пользователь, для
того, чтобы выдать себя за этого пользователя и подделать сессию. Таким образом,
представляется, что единственная защита - либо скрывать идентификатор
сессии, либо делать его трудным для угадывания (а лучше - и то, и другое).
PHP генерирует случайный идентификатор сессии, который практически невозможно
угадать, таким образом, об этом можно почти не заботиться. Предотвратить же
раскрытие атакующим правильного идентификатора сессии гораздо сложнее, так как
большая часть ответственности за это лежит вне области контроля
разработчика.
Есть много ситуаций, которые могут привести к раскрытию пользовательского
идентификатора сессии. GET-данные могут быть ошибочно кэшированы, замечены
случайным наблюдателем, сохранены в закладках или переданы по электронной почте.
Куки обеспечивают несколько более безопасный механизм, но пользователи могут
отключить их поддержку, исключая возможность их использования, а уязвимости
браузеров известны случайной утечкой информации из кук на несанкционированные
сайты (смотрите www.peacefire.org/security/iecookies/ и www.solutions.fi/iebug/
для дополнительной информации).
Итак, разработчик может быть вполне убеждён, что идентификатор сессии не
может быть угадан, однако возможность, что он может быть выведан атакующим,
гораздо более вероятна независимо от метода, использующегося для его передачи.
Нужны какие-то дополнительные средства для предотвращения ВСД.
На практике типичный HTTP-запрос помимо Host содержит множество
необязательных заголовков. К примеру, рассмотрим следующий:
GET /
HTTP/1.1 Host: www.example.org Cookie:
PHPSESSID=12345 User-Agent: Mozilla/5.0 Galeon/1.2.6 (X11; Linux
i686; U;) Gecko/20020916 Accept: text/html;q=0.9,
*/*;q=0.1 Accept-Charset: ISO-8859-1, utf-8;q=0.66,
*;q=0.66 Accept-Language: en
Этот запрос содержит четыре необязательных заголовка: User-Agent,
Accept, Accept-Charset и Accept-Language. Из-за того,
что заголовки эти необязательны, будет не очень разумно надеяться на их наличие.
Однако если браузер пользователя всё же послал эти заголовки, безопасно ли
полагать, что они будут присутствовать и в последующих запросах от того же
браузера? Ответом является "да" с очень немногими исключениями. Предполагая, что
предыдущий пример - это запрос, посланный пользователем с активной сессией,
рассмотрим следующий запрос, посланный незадолго после этого:
GET /
HTTP/1.1 Host: www.example.org Cookie:
PHPSESSID=12345 User-Agent: Mozilla/5.0 (compatible; IE 6.0
Microsoft Windows XP)
Так как предоставлен тот же уникальный идентификатор, будет получен доступ к
той же PHP-сессии. Если браузер идентифицирует себя иначе, чем в предыдущем
взаимодействии, надо ли полагать, что это тот же пользователь?
Надеюсь вам понятно, что это не желательно, тем не менее, это именно то, что
произойдёт, если вы пишете код, который не проверяет специально такие ситуации.
Даже в случаях, когда вы не можете быть уверены, что запрос является ВСД-атакой,
простое требование ввода пользователем пароля может помочь предотвратить ВСД без
причинения особого неудобства вашим пользователям.
Вы можете добавить проверку браузера пользователя в вашу модель безопасности
с помощью кода, подобного представленному на листинге 3.
Листинг 3
<?php
session_start();
if ( md5($_SERVER['HTTP_USER_AGENT']) != $_SESSION['HTTP_USER_AGENT'] ) {
/* Запрос пароля */
exit; }
/* Остальной код */
Конечно, перед этим вам необходимо сохранять MD5-хэш от полного названия
браузера пользователя всякий раз, когда вы впервые начинаете сессию, как
показано в листинге 4.
Листинг 4
<?php
session_start();
$_SESSION['HTTP_USER_AGENT'] = md5($_SERVER['HTTP_USER_AGENT']);
Хотя и не обязательно, чтобы вы использовали MD5-хэш вместо полного названия
браузера пользователя, это поможет обеспечить определённое постоянство и
исключить необходимость проверки правильности значения
$_SERVER['HTTP_USER_AGENT'] перед сохранением его в сессии. Так как
данные эти берутся от клиента, доверять им вслепую нельзя, но формат MD5-хэша
независим от входных данных.
Теперь, когда мы добавили проверку браузера пользователя, атакующий должен
осуществить два шага для того, чтобы подделать сессию:
- Получить правильный идентификатор сессии.
- Предоставить при ВСД-атаке такой же заголовок User-Agent.
Хоть это, несомненно, и возможно, по крайней мере, это сложнее, чем, если бы
второй шаг был пропущен. Таким образом, мы уже несколько усилили безопасность
сессионного механизма.
Другие заголовки могут быть добавлены таким же путём, и вы можете даже
использовать в качестве "отпечатков пальцев" комбинацию заголовков. Если вы
также добавите некий дополнительный секретный префикс, то такие "отпечатки"
становится практически невозможно угадать. Посмотрите пример на листинге 5.
Листинг 5
<?php
session_start();
$fingerprint = 'SECRETSTUFF' . $_SERVER['HTTP_USER_AGENT'] . $_SERVER['HTTP_ACCEPT_CHARSET']; $_SESSION['fingerprint'] = md5($fingerprint . session_id());
Заголовок Accept не должен использоваться в "отпечатках пальцев",
так как Microsoft'овский Internet Explorer известен расхождением в значениях
этого заголовка в случаях, когда пользователь обновляет страницу и когда он
кликает по ссылке.
С трудно угадываемыми "отпечатками пальцев" некоторый выигрыш достигается и
без усложнения этой информации иным, чем демонстрировалось до сих пор, способом.
При существующем механизме для ВСД по сути требуются те же два шага, хотя второй
шаг теперь гораздо сложнее, так как атакующий должен воспроизвести множественные
заголовки.
Для увеличения безопасности необходимо начать включать данные в дополнение к
уникальному идентификатору. Рассмотрим механизм управления сессией, при котором
уникальный идентификатор передаётся в GET-данных. Если "отпечатки пальцев",
сгенерированные в предыдущем примере, также передаются как GET-данные, атакующий
должен осуществить следующие три шага для успешной подделки сессии:
- Получить правильный идентификатор сессии.
- Предоставить те же HTTP-заголовки.
- Предоставить правильные "отпечатки пальцев".
Если и уникальный идентификатор, и "отпечатки пальцев" передаются как
GET-данные, то, возможно, что атакующий, который сможет получить что-то одно из
них, также получит доступ и к другому. Более безопасным подходом является
использование двух различных методов передачи - GET-данных и кук. Конечно,
это зависит от пользовательских настроек, но для тех, кто разрешил куки, может
быть обеспечен дополнительный уровень защиты. Таким образом, если атакующий
получит уникальный идентификатор через уязвимость браузера, "отпечатки пальцев",
вероятно, всё ещё будут ему неизвестны.
Есть много других способов, которые могут быть использованы, чтобы увеличить
безопасность вашего механизма управления сессией. Будем надеяться, вы добьётесь
успеха на пути создания некоторых собственных техник. Как-никак, именно вы
являетесь экспертом ваших собственных приложений, поэтому, вооружённые хорошим
пониманием сессий, вы - самый подходящий человек для реализации
дополнительной безопасности.
Неизвестность для постороннего (НДП)
Я хотел бы рассеять распространённый миф об НДП. Состоит он в том, что якобы
посредством такой неизвестности нельзя обеспечивать безопасность. Как
упоминалось ранее, НДП не является чем-то, что предлагает адекватную защиту, и
на что стоит полагаться. Тем не менее, это не означает, что с помощью такой
неизвестности совершенно не может быть обеспечена безопасность. Наоборот, уже
имея в качестве основы безопасный механизм управления сессией, НДП может
предложить небольшую степень дополнительной надёжности.
Помочь может простое использование вводящих в заблуждение имён переменных для
уникального идентификатора и "отпечатков пальцев". Вы можете также передавать
ложные данные, для того чтобы сбить с пути потенциального атакующего и ввести
его в заблуждение. На эти техники для защиты, безусловно, полагаться никогда не
стоит, но вы не даром потратите время, если организуете немного НДП в вашем
собственном механизме. Для тех, у кого нет базового понимания безопасности
сессий, вероятно, лучше всего поддерживать миф об НДП, иначе кто-нибудь может
быть введён в заблуждение, поверив в то, что она даёт достаточный уровень
защиты.
Резюме
Я надеюсь, что вы извлекли кое-что для себя из этой статьи. В особенности,
теперь у вас должно быть базовое понимание того, как работает web, как
достигается возможность сохранения состояния, чем в действительности являются
куки, как работают PHP сессии и некоторые техники, которые вы можете
использовать для улучшения надёжности ваших сессий.
Если у вас есть какие-либо вопросы или комментарии, - моя контактная
информация доступна на моём web-сайте shiflett.org ; также вы
можете размещать ваши отзывы на эту статью в форуме PHP Magazine'а forum.php-mag.net. Я хотел бы
услышать о ваших собственных методах безопасного управления сессией и я надеюсь,
что эта статья обеспечит вводную информацию, которая понадобится вам для
поддержки собственного творчества.
Ссылки и литература
Существует много дополнительных ресурсов, доступных по этому вопросу.
Несколько известных и свободно доступных в web'е изложены ниже:
Автор: Chris Shiflett
Перевод: Бресь Сергей
Источник: www.detail.phpclub.net
|