Как правильно добавить токен подделки межсайтовых запросов (CSRF) с помощью PHP

98

Я пытаюсь повысить безопасность форм на моем веб-сайте. Одна из форм использует AJAX, а другая представляет собой простую форму «свяжитесь с нами». Я пытаюсь добавить токен CSRF. Проблема, с которой я сталкиваюсь, заключается в том, что токен только иногда появляется в «значении» HTML. В остальное время значение пустое. Вот код, который я использую в форме AJAX:

PHP:

if (!isset($_SESSION)) {
    session_start();
$_SESSION['formStarted'] = true;
}
if (!isset($_SESSION['token']))
{$token = md5(uniqid(rand(), TRUE));
$_SESSION['token'] = $token;

}

HTML

 <form>
//...
<input type="hidden" name="token" value="<?php echo $token; ?>" />
//...
</form>

Какие-либо предложения?

Кен
источник
Просто любопытно, для чего token_timeиспользуется?
zerkms 09
@zerkms Я сейчас не использую token_time. Я собирался ограничить время, в течение которого токен действителен, но еще не полностью реализовал код. Для ясности я удалил его из вопроса выше.
Кен
1
@Ken: чтобы пользователь мог получить случай, когда он открыл форму, опубликовал ее и получил недействительный токен? (поскольку он был признан недействительным)
zerkms 09
@zerkms: Спасибо, но я немного запутался. Есть ли шанс дать мне пример?
Кен
2
@Ken: конечно. Предположим, токен истекает в 10:00. Сейчас 09:59. Пользователь открывает форму и получает токен (который все еще действителен). Затем пользователь заполняет форму в течение 2 минут и отправляет ее. Пока сейчас 10:01 - токен считается недействительным, поэтому пользователь получает ошибку формы.
zerkms 09

Ответы:

298

Для кода безопасности не создавайте токены таким образом: $token = md5(uniqid(rand(), TRUE));

Попробуйте это:

Создание токена CSRF

PHP 7

session_start();
if (empty($_SESSION['token'])) {
    $_SESSION['token'] = bin2hex(random_bytes(32));
}
$token = $_SESSION['token'];

Sidenote: Один из проектов с открытым исходным кодом моего работодателя является инициативой портировать random_bytes()и random_int()в PHP 5 проектов. Он лицензирован MIT и доступен на Github и Composer как paragonie / random_compat .

PHP 5.3+ (или с ext-mcrypt)

session_start();
if (empty($_SESSION['token'])) {
    if (function_exists('mcrypt_create_iv')) {
        $_SESSION['token'] = bin2hex(mcrypt_create_iv(32, MCRYPT_DEV_URANDOM));
    } else {
        $_SESSION['token'] = bin2hex(openssl_random_pseudo_bytes(32));
    }
}
$token = $_SESSION['token'];

Проверка токена CSRF

Не просто используйте ==или даже ===используйте hash_equals()(только PHP 5.6+, но доступно для более ранних версий с библиотекой hash-compat ).

if (!empty($_POST['token'])) {
    if (hash_equals($_SESSION['token'], $_POST['token'])) {
         // Proceed to process the form data
    } else {
         // Log this as a warning and keep an eye on these attempts
    }
}

Идем дальше с токенами Per-Form

Вы можете дополнительно ограничить доступность токенов только для определенной формы, используя hash_hmac(). HMAC - это особая хэш-функция с ключом, которую безопасно использовать даже с более слабыми хэш-функциями (например, MD5). Однако я рекомендую вместо этого использовать семейство хэш-функций SHA-2.

Сначала сгенерируйте второй токен для использования в качестве ключа HMAC, а затем используйте такую ​​логику для его визуализации:

<input type="hidden" name="token" value="<?php
    echo hash_hmac('sha256', '/my_form.php', $_SESSION['second_token']);
?>" />

А затем, используя конгруэнтную операцию при проверке токена:

$calc = hash_hmac('sha256', '/my_form.php', $_SESSION['second_token']);
if (hash_equals($calc, $_POST['token'])) {
    // Continue...
}

Токены, созданные для одной формы, не могут быть повторно использованы в другом контексте без ведома $_SESSION['second_token']. Важно, чтобы в качестве ключа HMAC вы использовали отдельный токен, а не тот, который вы просто добавляете на страницу.

Бонус: гибридный подход + интеграция Twig

Любой, кто использует механизм создания шаблонов Twig, может воспользоваться упрощенной двойной стратегией, добавив этот фильтр в свою среду Twig:

$twigEnv->addFunction(
    new \Twig_SimpleFunction(
        'form_token',
        function($lock_to = null) {
            if (empty($_SESSION['token'])) {
                $_SESSION['token'] = bin2hex(random_bytes(32));
            }
            if (empty($_SESSION['token2'])) {
                $_SESSION['token2'] = random_bytes(32);
            }
            if (empty($lock_to)) {
                return $_SESSION['token'];
            }
            return hash_hmac('sha256', $lock_to, $_SESSION['token2']);
        }
    )
);

С помощью этой функции Twig вы можете использовать оба токена общего назначения следующим образом:

<input type="hidden" name="token" value="{{ form_token() }}" />

Или заблокированный вариант:

<input type="hidden" name="token" value="{{ form_token('/my_form.php') }}" />

Twig занимается только отображением шаблонов; вы по-прежнему должны правильно проверять токены. На мой взгляд, стратегия Twig предлагает большую гибкость и простоту, сохраняя при этом возможность максимальной безопасности.


Одноразовые токены CSRF

Если у вас есть требование безопасности, согласно которому каждый токен CSRF может быть использован ровно один раз, самая простая стратегия - восстановить его после каждой успешной проверки. Однако это приведет к аннулированию каждого предыдущего токена, что плохо сочетается с людьми, которые просматривают несколько вкладок одновременно.

Paragon Initiative Enterprises поддерживает библиотеку Anti-CSRF для этих критических случаев. Он работает исключительно с одноразовыми токенами формы. Когда в данных сеанса сохранено достаточное количество токенов (конфигурация по умолчанию: 65535), сначала будут циклически удаляться самые старые невыкупленные токены.

Скотт Арцишевски
источник
хорошо, но как изменить токен $ после того, как пользователь отправил форму? в вашем случае один токен, используемый для пользовательского сеанса.
Akam
1
Посмотрите внимательно, как реализован github.com/paragonie/anti-csrf . Жетоны одноразовые, но в них хранится несколько.
Скотт Аркишевски,
@ScottArciszewski Что вы думаете о создании дайджеста сообщения из идентификатора сеанса с секретом и последующего сравнения полученного дайджеста токена CSRF с повторным хешированием идентификатора сеанса с моим предыдущим секретом? Надеюсь, вы понимаете, о чем я.
MNR
1
У меня вопрос о проверке токена CSRF. I, если $ _POST ['token'] пуст, мы не должны продолжать, потому что этот почтовый запрос был отправлен без токена, верно?
Hiroki
1
Потому что он будет отражен в форме HTML, и вы хотите, чтобы он был непредсказуемым, чтобы злоумышленники не могли просто подделать его. Здесь вы действительно реализуете аутентификацию типа "запрос-ответ", а не просто "да, эта форма допустима", потому что злоумышленник может просто подделать ее.
Скотт Арцишевски
24

Предупреждение безопасности : md5(uniqid(rand(), TRUE))это небезопасный способ генерации случайных чисел. См. Этот ответ для получения дополнительной информации и решения, использующего криптографически безопасный генератор случайных чисел.

Похоже, вам нужен еще один с вашим if.

if (!isset($_SESSION['token'])) {
    $token = md5(uniqid(rand(), TRUE));
    $_SESSION['token'] = $token;
    $_SESSION['token_time'] = time();
}
else
{
    $token = $_SESSION['token'];
}
данные
источник
11
Примечание: я бы не стал доверять md5(uniqid(rand(), TRUE));контекстам безопасности.
Скотт Аркишевски
2

Переменная $tokenне извлекается из сеанса, когда находится там

Дэни
источник