Общая информация: функция confirm_referrer делает дополнительную проверку откуда пришли данные.
В движке есть несколько десятков мест где она используется — это обработчики POST форм. Функция использует переменную $_SERVER['HTTP_REFERER'].
Недостатки: HTTP_REFERER успешно подделывается хакерами. На форумах с ЧПУ возникают сложности с указанием скрипта для проверки, т.к. есть варианты типа edit/99 или edit.php?id=99.
Решение: классики советуют использовать некое случайно генерируемое поле в самой форме. Обработчик посылки должен сравнивать значение этого поля с ожидаемым.
В PunBB 1.3 для этого используют поле csrf_token. Оно генерируется при авторизации пользователя и сохраняется в таблице online. Можно сказать, что csrf_token это некий идентификатор текущей сессии пользователя. Сессия и token действительны пока пользователь не сделает logout или по таймауту (15мин бездействия). Значение этого поля подставляется во все POST формы вот так:
<input type="hidden" name="csrf_token" value="'.$pun_user['csrf_token'].'" />
Недостаток этого подхода в том, что достаточно как-то украсть это значение в любом месте (через JS-внедрение) и можно подделать любую другую форму, пока текущая сессия не сэкспарилась. Например, злопыхатель воспользовался дыркой в ЛС послав админу письмо с javascript и тутже вызывает от его имени скрипты admin_* — поле csrf_token одинаково в любом месте.
Я предлагаю усилить защиту за счет генерации уникального поля для каждой формы. В проверяемую форму вставляет такой код:
<input type="hidden" name="csrf_hash" value="<?php echo csrf_hash() ?>"
Функция csrf_hash() использует $pun_user['csrf_token'] и адрес скрипта $_SERVER['SCRIPT_NAME']. Адрес скрипта неизменный в ЧПУ. Таким образом мы убиваем трех зайцев: скрываем online.csrf_token, делаем привязку к конкретной POST форме и оставляем все вызовы confirm_referer() как они есть
Код функции:
// Make sure that form posted from right place
//
function confirm_referrer($script)
{
global $pun_user, $lang_common, $base_url;
if (isset($_POST['csrf_hash']))
$hash = $_POST['csrf_hash'];
else if (isset($_GET['csrf_hash']))
$hash = $_GET['csrf_hash'];
// Hash not found
else
message($lang_common['Bad referrer']);
eregi('^(https?\://)(www\.)?(.*)$', $base_url, $regs);
$script = $regs[3].'/'.$script;
$new_hash = pun_hash($pun_user['csrf_token'].$script);
// Hash is wrong
if ($new_hash != $hash)
message($lang_common['Bad referrer']);
}
//
// Produce token for post validation
//
function csrf_hash()
{
global $pun_user;
eregi('^(www\.)?(.*)$', $_SERVER['HTTP_HOST'], $regs);
$script = $regs[2].$_SERVER['SCRIPT_NAME'];
return pun_hash($pun_user['csrf_token'].$script);
}
Example of Usage:
// in POST ot GET form, for ex. profile.php
<input type="hidden" name="csrf_hash" value="<?php echo csrf_hash() ?>" />
...
// in target script
confirm_referrer('profile.php');
Кстати, можно смело заменить $pun_user['csrf_token'] на вызов session_id(), тогда не понадобится заводить новое поле в таблице online (но тогда надо где-то в common.php стартовать сессию по session_start() - читайте мануалы PHP)
В 1.3 новый алгоритм csrf_token
Используется не открытый csrf_token (другими словами id сессии), а некий ключ, производный от адреса страницы и id сессии. Это гарантирует, что украденная и сохраненная страница не может служить отмычкой ко всем остальным страницам.
Есть принципиальное отличие от пары csrf_hash()+confirm_referrer(): используется адрес страницы-получателя, а не страницы-отправителя. Это позволяет ему оставить проверку в одном месте - в common.php и при этом все вроде довольно секьюрно получается!
Меню сайта
Функция confirm_referrer в PunBB |