Як правильно додати маркер підробки між запитами (CSRF) за допомогою PHP


96

Я намагаюся додати певного захисту до форм на моєму веб-сайті. Одна з форм використовує 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

@zerkms, який я зараз не використовую token_time. Я збирався обмежити час, протягом якого токен діє, але ще не повністю реалізував код. Задля ясності я видалив це з вищезазначеного питання.
Кен

1
@Ken: щоб користувач міг отримати справу, коли він відкрив форму, опублікувати її та отримати недійсний маркер? (оскільки його було визнано недійсним)
zerkms

@zerkms: Дякую, але я трохи розгублений. Будь-який шанс ви могли б надати мені приклад?
Кен

2
@Ken: звичайно. Припустимо, термін дії маркера закінчується о 10:00. Зараз 9:59 ранку. Користувач відкриває форму та отримує маркер (який ще діє). Потім користувач заповнює форму протягом 2 хвилин і відправляє її. Поки зараз 10:01 - маркер вважається недійсним, таким чином користувач отримує помилку форми.
zerkms

Відповіді:


290

Для коду безпеки не генеруйте свої токени таким чином: $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
    }
}

Подальше використання жетонів за формою

Ви можете додатково обмежити маркери, доступні лише для певної форми, використовуючи 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:

$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), він спочатку вимкне найстаріші невикористані маркери.


приємно, але як змінити маркер $ після того, як користувач надіслав форму? у вашому випадку - один маркер, що використовується для сеансу користувача.
Акам

1
Подивіться уважно, як реалізовано github.com/paragonie/anti-csrf . Маркери одноразові, але в них зберігається кілька.
Скотт Арчішевський

@ScottArciszewski Що ви думаєте про те, щоб згенерувати дайджест повідомлення з ідентифікатора сесії із секретом, а потім порівняти отриманий дайджест токена CSRF із повторним хешуванням ідентифікатора сесії з моїм попереднім секретом? Сподіваюся, ви розумієте, що я маю на увазі.
MNR

1
У мене питання щодо перевірки маркера CSRF. Якщо $ _POST ['token'] порожній, ми не повинні продовжувати, оскільки цей запит на повідомлення було надіслано без маркера, так?
Хірокі

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не отримується із сеансу, коли вона там знаходиться

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.