Страницы

Поиск по вопросам

четверг, 4 октября 2018 г.

Вопросы безопасности PHP. Фильтрация входящих данных

В последнее время начал интересоваться безопасностью создаваемых приложений, интересует несколько моментов связанных с фильтрацией данных в PHP и безопасной аутентификацией/авторизацией. В гуглах нахожу много разной информации, но её не хватает чтобы сделать выводы. Возможно кто-то подскажет или направит на путь истинный, так сказать.
Главные вопросы:
Нужно ли мне фильтровать полностью все входные данные, в т.ч. глобальные массивы $_SERVER, $_REQUEST, $_GET, $_POST, $_COOKIE, даже при условии что они не вносятся в базу данных. Какие общие моменты мне стоит учесть? Чем лучше пользоваться filter_var(), filter_input()и т.д., или использовать регулярные выражения. Или когда лучше использовать одно вместо другого. Какой метод авторизации на сайте можно считать безопасным? Используя PDO, могу ли я не боятся связывать переменные сразу (я так никогда не делал, просто интересно, насколько безопасны такие действия) bindValue(':param', $_POST['value']); Если у меня есть HTML(wysiwyg) редактор, то нужно мне перед сохранение в БД использовать функции htmlspecialchars($var, ENT_QUOTES, 'UTF-8'); и htmlspecialchars_decode($var, ENT_QUOTES);
Что у меня сейчас:
Авторизация на сайте происходит так: Пользователь вводит логин/пароль. Идет запрос на сервер и пытается получить данные (id, пароль, уникальный хеш пользователя) по указаному логину. Если такой есть, то проверяется пароль функцией password_hash($password, PASSWORD_DEFAULT);, и в случае успеха создаются куки, и новый хеш для пользователя:
$user_hash = md5( md5( time() + time() * rand(2, 10) )); SessionModel::setCookie('_auth', md5($user_id), AUTH_TIMEOUT); SessionModel::setCookie('_token', $user_hash, AUTH_TIMEOUT);
Пока что я еще не придумал где и как использовать этот хеш с умом, чтобы подтверждать личность пользователя. Скорее всего безопасностью тут и не пахнет, по-этому и прошу советов.
Фильтрация данных:
Недели две назад полностью перешел ООП и начал использовать PDO, до этого использовал mysqli для соединения, соответственно чтобы очищать входящие данные, я писал свои функции, типа:
function clear($var) {
$link = mysqli_connect(HOST, USER, PASSWORD, DB) or die( mysqli_error($link)); $var = strip_tags($var); $var = htmlspecialchars($var); $var = mysqli_real_escape_string($link, strip_tags($var)); mysqli_close($link);
return $var; }
Сейчас я вообще не использую фильтрацию для входящих данных, кроме как для html кода, для этого я использую
$encoded = htmlspecialchars($var, ENT_QUOTES, 'UTF-8');
$decoded = htmlspecialchars_decode(htmlspecialchars_decode($var, ENT_QUOTES), ENT_QUOTES);
Последняя повторяется еще раз затем, что с первого раза оно почему-то нормально не отображало расшифрованные сущности, не знаю почему, но чисто случайно таким способом заработало. Остальные данные я принимаю как-то так:
$title = $_POST['title']; - иногда использую trim() чтобы убрать пробелы :))
В общем, понимаю что вряд ли на каждый из вопросов получу здесь развернутый ответ, но был бы очень благодарен, даже за актуальную на сегодняшний день статью с ответами или ответом на такие вопросы. Изучаю PHP уже где-то 1.5-2 года, а на самые простые вопросы (или не простые), ответов не знаю. В гугле же найти подобное трудно, как показала практика.
И буду рад общим рекомендациям :) Спасибо.


Ответ

Нужно ли мне фильтровать полностью все входные данные
Вы обязаны не доверять любым данным, полученным извне. Например, $_GET, $_POST (по-умолчанию они вдвоём составляют $_REQUEST, по настройкам php.ini request_order и variables_order сюда же могут входить куки, $_SERVER и переменные окружения), $_COOKIE, $_FILES, данные загружаемые со сторонних систем (например, по API). Общий момент - вы должны искать не абстрактный фильтр от опасных данных, а понимать, какие данные вы рассчитываете обнаружить в этом месте и что с этими данными будет происходить дальше. Вывод в CSV, HTML или запись в СУБД - каждый требует своей особой обработки.
Чем лучше пользоваться filter_var(), filter_input() и т.д., или использовать регулярные выражения.
Всем, что позволит вам убедиться в корректности данных. Начинать надо с белого списка. Нередко вы заранее знаете, что, например, $_GET['index'] у вас может быть только foo или только bar. Вот на эти два допустимых значения и проверяйте.
Например, для email пользователя есть штатная регулярка, скрытая в filter_var. Это хорошая отправная точка и обычно будет работать хорошо. "Обычно" - потому что email очень уж забавная вещь. Если почитать соответствующие RFC, то окажется, что проще проверить на содержание символа @ и отправить-таки письмо, чем разбираться во всём многообразии допустимых вариантов. Допустимо там практически всё.
Для, например, логина, вы можете пожелать ограничить ввод только латинскими буквами и некоторыми спецсимволами. Это проще всего делается регуляркой.
Самая широкая трактовка, обычно, для свободного текстового ввода. Например, вот для этого самого сообщения. Как правило, допустимы любые символы UTF8.
Кстати, раз завёл речь об этом: пожалуйста, никак не валидируйте пароль, кроме разве что по минимальной длине. И только если того однозначно требует предметная область, то по минимальной сложности. Но ни в коем случае не ограничивайте максимальную. Вам всё равно его хэшировать, а не хранить, пусть пользователь вводит то, что ему нравится и той длины, которая ему нравится.
Какой метод авторизации на сайте можно считать безопасным?
В зависимости от требования к безопасности. ЭЦП довольно сложно обойти (образно, банковская сфера). Сложно обойти, если авторизация разрешена только с одного конкретного IP одной конкретной VPN (корпоративные данные). Для сайта не столь чувствительного к безопасности - HTTPS (при верной настройке серверной стороны! За прошедшие года неправильно настроить HTTPS стало довольно просто) адекватно прикроет от MitM и зашифрует данные.
Можно хэшировать исходный пароль на клиенте и передать хэш на сервер, чтобы исходный пароль вообще по сети не передавался.
Без HTTPS? Сделайте HTTPS, времена дорогих сертификатов уже в прошлом.
Используя PDO, могу ли я не боятся связывать переменные сразу
SQL инъекции в этом случае нет. И сразу же важная оговорка: только если у вас корректно настроена кодировка соединения либо отключена эмуляция подготовленных выражений. https://stackoverflow.com/questions/134099/are-pdo-prepared-statements-sufficient-to-prevent-sql-injection
Но логические ошибки вы проверять всё равно должны. Например, как думаете, использовать (int) $_POST['amount'] безопасно в качестве :amount?
UPDATE users SET balance = balance - :amount WHERE id=:user
(пример, реально в таком месте будет двойная бухгалтерская запись, которая check'ом дополнительно валидируется элементарно на уровне записи в субд (особенно если бы тот же mysql вообще умел делать check) но как говорит один ДБА, на деньгах люди понимают быстрее).
А если передать -100? Получим начисление денег вместо списания?
Если у меня есть HTML(wysiwyg) редактор, то нужно мне перед сохранение в БД использовать функции
Очень интересный вопрос и поведение зависит от степени доверия. Вы доверяете тому, кто этим редактором пользуется? Т.е. на выходе должен быть реальный HTML и его необходимо выводить как HTML? Это частая вещь для админки какой-нибудь CMS. Тогда вы валидировать это поле не должны вообще никак. htmlspecialchars($var, ENT_QUOTES, 'UTF-8') должен вызываться для этого текста при подстановке в textarea, иначе случайный в тексте сломает всё.
Если вы не доверяете, но там будет HTML - то вы обязаны досконально разбирать на лексемы и проверять по белому списку весь переданный HTML. Конкретных инструментов не подскажу, знаю только, что есть такие. Проблема в том, что, например, вы хотите дать возможность вставлять , а вам подсунут какой-нибудь и всё, приехали. Вместо безобидного alert может быть и что поинтереснее. А htmlspecialchars нельзя, иначе и картинки не будет тоже.
Если HTML быть не должно вообще - то htmlspecialchars. Можно перед записью в базу применять, а логически уместнее применять непосредственно при выводе в HTML. Но не strip_tags. Почему это вы удаляете то, что пользователь ввёл? Вы должны это верно сохранить и верно показать, а не удалять.
Если такой есть, то проверяется пароль функцией password_hash($password, PASSWORD_DEFAULT);
Это ошибка в вопросе? password_hash ничего не проверяет. Проверяет password_verify.
Зачем вы что-то ой как далеко не CSPRNG пишете, судя по всему, в куки и как планируете использовать потом - я тоже не представляю.
CSPRNG - криптографически стойкий генератор псевдослучайных чисел.
Для сессионной авторизации сессии и используйте. Напомню только про один очевидный подводный камень, на который не всегда обращают внимание: у сессии нет времени жизни. Вообще нет. Есть только величина времени от последнего обращения к этой сессии, после которой эту сессию может удалить сборщик мусора. А когда сборщик мусора запустится - да кто его знает. И всё это время сессия всё ещё валидна. Поэтому если для вашей задачи необходимо инвалидировать авторизацию через час после авторизации или после последнего обращения пользователя - эту логику вы должны сделать сами.
Для долговременной авторизации - по-моему, этот ответ и так уже огромен. Лучше отдельным вопросом.
Фильтрация данных:
См. начало ответа. Вы должны знать, что вы хотите найти в этих данных и куда эти данные пойдут потом. Остальное к безопасности не относится, лишь костыли и иллюзии безопасности. Какой-то магической функции "сделай мне правильно и безопасно" нет.
И разумеется, вы не можете быть уверены, что такие данные вам вообще пришли. Сначала проверяйте на isset или, если допустимо для значений, empty. Или filter_input, он тоже корректно среагирует на отсутствующие ключи.
И раз тут уже про CSRF напомнили: запомните, что всё, что изменяет состояние системы - должно делаться через POST, PUT, PATCH или DELETE запросы (если речь не об API, то обычно используется только POST) и быть прикрыто уникальным токеном. Уникальным вообще или уникальным для пользователя или для сессии - вопрос уже дискуссионный. GET же запросы должны информацию только читать. Два одинаковых GET-запроса должны возвращать идентичный результат. От этого правила иногда приходится отступать, например для ссылки "отписаться от рассылки" в письмах (изменяет данные о подписках), но это именно исключение. Не надо делать удаление чего-нибудь через GET-запрос.

Комментариев нет:

Отправить комментарий