Безопасность

От базовых принципов до особенностей PHP

Александр Макаров

Yii core team

https://slides.rmcreative.ru/2021/efko-security/

Обо мне

Не эксперт по безопасности. Но кое-что знаю.

Я занимался аудитом кода многих проектов...

... и практически всегда встречал похожие ошибки.

Главная идея в том...

Что нельзя доверять данным не смотря ни на что

Фильтруй вход,
экранируй выход

Входы

  • Формы
  • Файлы
  • Заголовки HTTP ($_SERVER['HTTP_X_FORWARDED_FOR'] и т.д.)
  • User agent
  • ...

Выходы

  • Браузер
  • Консоль
  • База данных
  • ...

Проблемы?

  • Плохо фильтруем
  • Неправильно экранируем

Фильтровать?

Фильтровать = убедиться, что данные верны.

Предпочитайте белые списки

Входные данные не валидны пока не доказано обратное.

Используйте filter_var


$email = filter_var($email, FILTER_VALIDATE_EMAIL);
if ($email === false) {
    // email не торт...
}
// всё OK

или используйте надёжные библиотеки и фреймворки

Экранировать?

Сделать так, чтобы спецсимволы вели себя как нормальные символы.

Обычно префиксированием других спецсимволов.

На каждый выход свои правила экранирования.

Популярные угрозы

XSS

На страницу вставляется скрипт, который запускается в браузере пользователя.

Встречалось в большинстве проектов, с которыми я работал.


...
<div>
<?= $_GET['query'] ?>
</div>
...
                    

Вместо alert-а могут:

  • Сделать что-то от имени пользователя...
  • Одолжить денег у его друзей
  • Нарепостить экстремистских комментов
  • Утащить денег со счёта
  • ...

Две главных разновидности

  • Первого порядка - выполняется немедленно
  • Второго порядка - пишется в базу, выполняется потом

Решение

Экранируй!

Экранируем HTML

Если нужен только текст.


htmlspecialchars($content, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
                    

Делаем HTML безопасным

Если нужен именно HTML.

HTMLPurifier (http://htmlpurifier.org/)


$config = HTMLPurifier_Config::createDefault();
$purifier = new HTMLPurifier($config);
$clean_html = $purifier->purify($dirty_html);
                    

Не сохраняйте экранированные данные в базу

Они могут вам понадобиться в исходном виде. К тому же, базе доверять нельзя.

Кстати, разработчики Chromium выкинули XSS-фильтр...

XSS в админке не менее опасен...

CSRF

Сторонний сайт может отправить формы на ваш сайт от имени пользователя.

Решение

Использовать токены CSRF, использовать TLS/SSL.

CSRF-токены

  • В момент отображения к форме добавляется уникальный случайный токен.
  • Тот же токен пишется в хранилище (обычно в сессию).
  • После отсылки формы, токены сравниваются.

Same origin policy недостаточно!

  • Политика действует на XMLHTTPRequest
  • Она не запрещает запросы, а предотвращает получение результата

Не используйте GET для изменения состояния приложения


<img src="/logout" />
                    

Использование $_REQUEST - то же самое.

SQL инъекции

Выполнение произвольного SQL в БД проекта.


$email = $_POST['email'];

query("SELECT *
FROM user
WHERE email = '$email';");
                    

' OR 1
                    

UNION SELECT 1,'',3,4,5 INTO OUTFILE '1.php' --%20
UNION SELECT 1,LOAD_FILE('config.php'),3,4,5 --%20
                    

Решение

Экранируйте запросы.

Но делайте это правильно!

Ручное экранирование не пойдёт!

  • addslashes()
  • mysql_escape_string()
  • mysql_real_escape_string()
  • ...

Потому что

  • Можно его забыть
  • Контекст может быть не верен (значения, таблицы, столбцы)
  • Может быть не полным
  • ...

Используйте prepared statements

Есть и в PDO и нативных драйверах. Для PDO:


$stmt = $db->prepare("SELECT * FROM user WHERE email = :email");
$stmt->bindValue(':email', $email);
$user = $stmt->fetch();
                    

Имена таблиц и столбцов

Используйте белый список.


$allowedTables = ['user_comments', 'post_comments'];
if (!in_array($table), $allowedTables, true) {
    return false;
}
// делаем запрос
                    

Отсутствие валидации загрузок

Возможность загрузить и запустить код.

  • Валидируйте mime
  • Пересохраняйте изображения

Инъекция кода

Возможность выполнить произвольный код.


eval($_GET['query']);

Решение

Не использовать eval() или использовать после проверки по белому списку.

Запускайте PHP с минимально возможными правами

Небезопасные include


require $_GET['type'] . '.php';
Белый список.

Clickjacking

Обманом заставить пользователя кликнуть на что-то на стороннем сайте.

iframe + opactity: 0

Не особо относится к PHP, но довольно серьёзно.

Решение

Запретить запихивать сайт в фреймы через RFC 7034:


header('X-Frame-Options: DENY');
// или
header('X-Frame-Options: SAMEORIGIN');
                    
или через JavaScript:

if (window.top !== window.self) {
    document.write = "";
    window.top.location = window.self.location;
    setTimeout(function () {
        document.body.innerHTML = '';
    }, 1);
    window.self.onload = function (evt) {
        document.body.innerHTML = '';
    };
}

Пароли

В чём проблема?

Зло может угадать ваш пароль

  • Перебором
  • Атакой по словарю
  • Атакой на основе времени ответа

Решение: ограничить попытки

  • 10 попыток с IP в минуту более чем достаточно
  • CAPTCHA

Всё ещё проблема?

  • Легко сменить IP
  • БД иногда утекают...

Важно ли, как хранить пароль?

Да!

Вы не должны знать пароль пользователя!

Храните хэш.

md5, sha1, sha256, sha512 и т.д. - не вариант

Даже с солью.

Хэши предназначены для быстрого вычисления

Полный перебор пароля из 8 символов, SHA-256 занимает...

3.5 дня на одной GPU 2011 года

На крутой GPU 2015 года это будет уже 20 часов

8x Nvidia GTX 1080 — 2.5 часа

GPU?

Я на ферме
GPU - это круто!

Используйте как минимум bcrypt

  • Изначально сделан не быстрым.
  • Заставляет работать как CPU, так и GPU.
  • Плохо считается на GPU (требует много доступа к памяти, что портит параллелизм).

Cost и сложность пароля важны

Cost?

итерации key derivation function = 2^cost

12+ - безопасное значение. Yii использует 13.

Немного посчитаем

Cost = 13 → ~28 хэшей в сек. на Nvidia GTX Titan X ($700, 2015)

28 * 60 * 60 * 24 = 2419200 хэшей в день

6 символьный пароль, буквы в нижнем регистре = 308915776 комбинаций

308915776 / 2419200 = 127 дней для перебора одного пароля

21 день с 6-ю видеокартами. Стоит это более ~4200$

Добавим в пароль из 6 символов цифры и стоить взлом за 21 день будет уже более ~22600$

GPU становятся все лучше и всё дешевле.

AMD R380X (2016) даёт 14 хешей bcrypt с cost=13 в секунду и стоит около $78.

У не случайных нападающих железо сильно лучше

bcrypt даёт вам лишь время

Если вы знаете, что хэши утекли:

  • Устранить источник утечек
  • Инвалидировать хэши
  • Попросить пользователей поменять пароли и объяснить, что сделать это надо везде

Подсказывайте сложные пароли, но не ограничивайте их

У вас будет больше времени среагировать на утечку.

Много времени можно получить используя Argon2

Не для паролей

  • md5, sha1, sha256, sha512
  • PBKDF2

Сессии

  • Фиксация - дать пользователю заранее известный ID сессии.
  • Перехват cookie.

Решение

Используйте только куки. В php.ini:

  • session.use_cookies = 1
  • session.use_only_cookies = 1
  • session_cookie_httponly = 1
  • session.cookie_secure = 1

Регенерируйте ID сессии при помощи session_regenerate_id(true) после логина или смены прав.

Не забывайте проверять доступ

Да, такое бывает.

  • Доступ к файлу по URL.
  • Скрыть != обезопасить.

Сначала запрещайте всё, потом уже разрешайте.

Утечки информации

  • Отладочный режим
  • Страницы ошибок
  • Ответы API
  • Системы контроля версий

Утечки паролей

  • Из репозитория
  • Из файлов

Что делать?

HashiCorp Vault

gitleaks GitHub action

2FA

Have I been pwned

Время жизни паролей (но тут засада)

Случайные числа

Используются для токенов, кодов сброса, генерируемых паролей, UUID и др.

Проблема

Случайные числа могут генерироваться предсказуемо или могут повторяться если источник не достаточно хорош.

Токены, коды сброса, генерируемые пароли

Решение

Использовать нормальные источники случайных чисел

Yii 2 использует

  • PHP 7 random_bytes
  • LibreSSL
  • CryptGenRandom (под Windows)
  • /dev/random (под FreeBSD)
  • /dev/urandom
  • Если ничего не получилось, кидаем исключение

Осторожней с библиотеками

Проводите тесты

Обновляйтесь

  • Linux
  • nginx
  • PHP
  • СУБД
  • Любимый фреймворк
  • ...

Но помните, что зависимости могут быть с уязвимостями...

Или с закладками...

Не доверяй... проверяй!

DDOS

  • Планируйте заранее.
  • Положитесь на firewall (возможно железный).
  • Может быть очень тяжело.
  • Часто это просто боты и их можно убить fail2ban и CloudFlare.

Держите про запас отменного админа...

Раз зашла речь про админов

  • Торчащие наружу memcached и mongodb
  • Саппорт Windows: активировали винду без проверки
  • Саппорт hetzner: ребут сервера
  • Раздача прав на прод всем разработчикам компании
  • Сервера в разных ДЦ, API без паролей
  • Не защищённый S3-бакет
...

Безопасность - это процесс

Нельзя сделать приложение безопасным один раз и навсегда.

Обучайте свою команду

Идеально, если в команде есть кто-то, кто знаком с темой. Используйте VCS и делайте code review.

Подумайте о bug bounty

Абсолютная безопасность недостижима

Люди — всегда слабое звено

  • Теряют флешки
  • Теряют ноуты
  • Теряют телефоны
  • Пропускают в офис кого попало
  • А там...

Планируйте заранее

  • Что есть злыдни получили доступ к X?
  • Какие данные не должны утечь никогда? (= не надо их хранить)
  • Следует ли админу потушить взломанный сервер?
  • Заготовьте страницу-заглушку.
  • Что сломали? Как? Каков урон?
  • Как поправить?
  • Объяснить команде, в чём была ошибка и как её не делать. Как альтернатива - найти технический способ не допускать подобных ошибок.

Постоянно учитесь

  • target="_blank" не безопасен
  • ImageMagick не безопасен
  • SMS не безопасны
  • XML тоже! (XXE)
  • Ох, и десериализовывать опасно!
  • Процессоры не безопасны :(
  • ...

Сделайте так, чтобы взломать вас было слишком хлопотно

Оставайтесь в безопасности

Почитать

Время вопросов!