Для самописного фреймворка продумываю защиту от csrf-атак. Подскажите, правильно ли она сделана в этом примере: PHP: <?php function getRandomString($length = 15) { $chars = '1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; $numChars = strlen($chars); $string = ''; for ($i = 0; $i < $length; $i++) { $string .= substr($chars, rand(1, $numChars) - 1, 1); } return $string; } session_start(); if ($_POST) { echo '<pre>'; print_r($_POST); echo '</pre>'; if ($_POST['csrf'] === $_SESSION['csrf']) { echo '<p>Токены идентичны. Ваши данные приняты.</p>'; } else { echo '<p>Токены не совпадают. Ваши данные отклонены.</p>'; } } else { $_SESSION['csrf'] = getRandomString(); } ?> <form method="POST" action=""> <input type="hidden" name="csrf" value="<?= $_SESSION['csrf'] ?>"> <input type='text' maxlength='15' value='' name='Name' /> <input type="submit"> </form> Результат отправки данных:
В принципе да. Как простейшую. У php есть свои функции случайных строк. https://secure.php.net/manual/ru/function.openssl-random-pseudo-bytes.php
Неправильная реализация, токен не одноразовый --- Добавлено --- И если открыть две вкладки, работать будет только одна
да не должен он быть одноразовым. Достаточно ему быть односессионным. Одноразовость - излишнее усложнение, не дающее никакого профита. Другое дело, что сессии не должны быть "вечными".
@Fell-x27 судя по коду, ТС. пытался сделать его одноразовым, но получилось, что токен затирается при рефреше, но сохраняется при выполнении действия. Про одноразовость. Допустим, пользователь отправил кому-то ссылку из адресной строки, содержащую токен. Если он одноразовый, то это означает, что он уже отыграл при рендере страницы и теперь бесполезен.
и не должен, токен берется из сессии в скрытое поле и параллено отправляется с основными данными, если это то что хотел тс.
не знаю что за простой рефреш, но пост данные не должны рефрешится и должен быть хедер с редиректом автоматом.
Я бы всё-таки привязал токен к конкретному экшену/форме, чтобы разные формы одного пользователя не конкурировали ))) То есть не $_SESSION['csrf'], а $_SESSION['csrf']['action_X']
Да, есть такая недоработка Сделал такой вариант, можно открывать несколько страниц с формой - везде будет корректная проверка. Но не знаю, на сколько этот вариант "кошерный" - при каждом обновлении страницы с формой создается новая запись с идентификатором и хешем формы: PHP: <?php function getRandomString($length = 15) { $chars = '1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; $numChars = strlen($chars); $string = ''; for ($i = 0; $i < $length; $i++) { $string .= substr($chars, rand(1, $numChars) - 1, 1); } return $string; } session_start(); if ($_POST) { $token = explode('_', $_POST['csrf']); if (hash_equals($token[0].'_'.$token[1].'_'.$token[2], $_SESSION['csrf'].'_'.$token[1].'_'.$_SESSION['form'][$token[1]])) { echo '<p>Токены идентичны. Ваши данные приняты.</p>'; } else { echo '<p>Токены не совпадают. Ваши данные отклонены.</p>'; } } else { // Создаем ключ сесси, если его нет. Не перезаписывается if (!isset($_SESSION['csrf'])) { $_SESSION['csrf'] = getRandomString(); } // Создаем массив с хешами форм - при каждом обновлении страницы с формой создается новая запись со своим хешем for ($i = 0;; $i++) { if (!isset($_SESSION['form'][$i])) { $_SESSION['form'][$i] = getRandomString(); break; } } } ?> <form method="POST" action=""> <input type="hidden" name="csrf" value="<?= $_SESSION['csrf'].'_'.$i.'_'.$_SESSION['form'][$i] ?>"> <label for="name">Имя:</label> <input id="name" maxlength='15' value='' name='Name' /> <input type="submit"> </form>
Ну я столкунлся с такой проблемой на ранних релизах Yii2 с его CSRF-защитой (сейчас этого вроде не наблюдается). Чувак открыл в сделанной мной CMS статью в админке на редактирование, а в соседней вкладке - сайт, чтоб взять ссылки на перелинковку, и в результате статья не добавилась - открывшийся сайт перезаписал CSRF-токен в сессии. Сейчас как-то по-хитрому решено это в Yii2, я недавно трассировал его, но там так сразу и не въехать, хотя по строчкам решение не много занимает.
Вот я и говорю, гумно эти ваши одноразовые хитровыбоенные формо-зависимые уникальные токены, и кроме геморроя и дутых проблем ничего не несут. Профиту с них, что коту с электробритвы. Люди упарываются, боясь, что в реальном времени кто-то тырить будет эти токены, забывая, что если кто-то имеет доступ к клиентской машине на таком уровне, то ему проще будет стырить явки-пароли, или напрямую действовать от имени клиента втихоря, а не тырить csrf-токены. csrf-токены это про рандомные нетаргетированные атаки через хитрые ссылочки.
код из первого поста будет создавать проблемы если открыто несколько форм. вот о какой. Ну буду ничего утверждать также безапеляционно Просто замечу, что используются разные техники. Полезное: https://habrahabr.ru/post/318748/
Второй вариант вам нравится? Там такой проблемы нет. Хотя мой внутренний перфекционизм не доволен ни первым, ни вторым вариантом - смотрю сейчас разные микро-фреймворки - как в них реализована защита от csrf-атак. В некоторых её вообще нет)
Ну тогда надо исправлять корень проблемы, а не лепить костыли на следствия --- Добавлено --- Я бы не был столь уверен в полезности этого материала, если честно. Там рассказывается, как с помощью адронного коллайдера гвоздь в фанерку забить.
ой всё! Фел, ты молодец. ок? и идём дальше. самоочевидно, что дырка в защите в одном месте обнуляет всю защиту целиком. если есть возможность присунуть исполняемый JS куда-нибудь, то аккаунт админа будет взломан с вероятностью 103%. НО, сцуко, всегда есть "но". в каких-то случаях токен может быть просран и без JS-инъекции, просто по невнимательности или в силу обстоятельств, которые изначально не были рассмотрены. цитирую https://ru.wikipedia.org/wiki/Межсайтовая_подделка_запроса : «…но следует иметь в виду, что спецификация HTTP/1.1 [3] допускает наличие тела для любых запросов, но для некоторых методов запроса (GET, HEAD, DELETE) семантика тела запроса не определена, и должна быть проигнорирована. Поэтому ключ может быть передан только в самом URL, или в HTTP заголовке запроса. Необходимо защитить самого пользователя от неблагоразумного распространения ключа, в составе URL…» я здесь вижу увеличение вероятности случайного просера токена. и если он один универсальный на любые действия пользователя в пределах сессии, это уже выглядит опасно. --- Добавлено --- или, например, операция logout. есть старая добрая шутка, когда подсовывают ссылку на "картинку" с адресом который всех посетителей разлогинит. чтобы защититься, надо подмешать токен. здесь ссылка выглядит так: https://php.ru/forum/logout/?_xfToken=23761/1519923172-6fa53b729f9ee86d88b8a04d9ea0b72935463560 (ойбля, я только что просрал свой токен. а ведь я модератор. хакеры сейчас воспользуются) --- Добавлено --- так что лучше без категоричных заявлений. всегда есть "но" и особые обстоятельства и хрена ты их все заранее учтёшь.
ну, на нормальных проектах вход в админку находится на другом домене - токен с основного домена тут никак не поможет. плюс к этому, самое важное - хеш от пароля хранится в куках P.S. Вы никак не ответили на мой вопрос:
Ок, я модератор, я могу выпилить к чертовой матери весь форум мановением мышки. Но вход в мой аккаунт не на отдельном домене. --- Добавлено --- это еще одна вещь, которую делать нельзя.
А зачем его передавать GET-запросом? --- Добавлено --- Из той же статьи в вики: --- Добавлено --- Ну, я категорично заявляю, что в данном случае @Fell-x27 прав, потому как нет ни одного разумного довода в пользу обратного )
зачем его передавать DELETE запросом понятно? он там тоже упоминается "нет сведений" == "я не видел" / "я игнорирую сведения". потому что довольно много людей считают иначе. --- Добавлено --- друзья, если кратко, лично мне насрать считаете ли вы достаточным тот или иной метод защиты или считаете ли вы достаточным, когда кто-то приводит иные аргументы. ваша защита это ваша проблема. с моей т.з. защита может быть только откровенно плохой или вроде бы хорошей. и не бывает гарантированно достаточной. --- Добавлено --- а должен был? при беглом взгляде мне не понравилось. как-то криво там со счётчиком $i, а вникать и исправлять лень.