Какие угрозы безопасности существуют при работе с данными в SQL
Что проверяют
Знаете ли вы, как конкретно защищается PHP-код, а не только слово «инъекция». Следом почти всегда просят показать безопасный запрос через PDO и спрашивают, что делать с именем колонки в ORDER BY, которое параметром не передашь.
Как отвечать
Начните с главной угрозы и её защиты, потом остальное:
- SQL-инъекция: пользовательский ввод склеивается со строкой запроса. Защита: подготовленные запросы с плейсхолдерами,
$pdo->prepare('SELECT * FROM users WHERE id = ?')иexecute([$id]). В Laravel то же даёт построитель запросов, опасныDB::raw(),whereRaw()иorderByRaw()с конкатенацией; - идентификаторы: имена таблиц, колонок и направление сортировки не биндятся, их проверяют по белому списку;
- инъекция второго порядка: данные безопасно сохранили, а потом достали из базы и вклеили в другой запрос строкой;
- лишние права: приложение ходит в базу пользователем с правами на
DROPиGRANT. Правильно: отдельный пользователь с минимальными правами, миграции под другой учёткой; - пароли: не шифруются и не хешируются
md5(), а хешируются черезpassword_hash()и проверяютсяpassword_verify(); - утечка через ошибки: текст исключения с запросом уходит пользователю. В проде
display_errors=Off, ошибки пишутся в лог; - секреты и дампы: пароль от базы в репозитории, дамп в публичной папке веб-сервера, соединение с базой без TLS.
Полезно знать детали. С PHP 8.0 PDO по умолчанию бросает исключения (PDO::ERRMODE_EXCEPTION). У драйвера MySQL по умолчанию включена эмуляция подготовленных запросов (PDO::ATTR_EMULATE_PREPARES), поэтому кодировку задают в DSN, charset=utf8mb4, а не отдельным SET NAMES.
Где спотыкаются
Считают защитой mysqli_real_escape_string() или addslashes(). Экранирование не спасает числовой параметр без кавычек и ломается при неверно заданной кодировке.
Думают, что ORM закрывает вопрос целиком. Любой сырой фрагмент в Eloquent или Doctrine снова открывает дыру.
Говорят «пароли шифруем». Шифрование обратимо, для паролей нужен медленный хеш с солью, и password_hash() делает это сам.