«А я просто экранирую кавычки»: почему ваш код уязвим
Если вы когда-нибудь гуглили «How can I sanitize user input with PHP?», вы не одиноки. Этот вопрос на Stack Overflow собрал сотни тысяч просмотров. И неудивительно: тема безопасности — одна из самых горячих в веб-разработке.
Но вот парадокс: большинство ответов на тот вопрос либо устарели, либо опасны. Разработчики до сих пор пишут что-то вроде mysql_real_escape_string() (которой уже 10 лет как нет) или просто удаляют «опасные» символы. Спойлер: это не работает.
В этой статье мы разберём топ-5 ошибок санитации пользовательского ввода в PHP, которые превращают ваш сайт в решето, и покажем, как их исправить раз и навсегда.
Ошибка №1: Слепая вера в «санитацию» как универсальную таблетку
Самая большая ошибка — думать, что существует одна функция, которая «очистит» любые данные. Вот типичный код новичка:
<?php
$name = $_POST['name'];
$name = strip_tags($name);
$name = addslashes($name);
// Теперь данные безопасны? Нет!
?>
Проблема в том, что разные контексты требуют разной обработки. Валидация (проверка формата) и санитация (очистка) — это не одно и то же. А ещё есть экранирование вывода, которое вообще происходит в другом месте.
Как надо: Определите, что вы делаете с данными на каждом этапе. Для базы данных — одно, для HTML — другое, для JSON — третье.
Ошибка №2: Использование устаревших функций (или их неправильное применение)
Давайте пройдёмся по «динозаврам» PHP-безопасности:
mysql_real_escape_string()— удалена в PHP 7.0. Забудьте.addslashes()— не спасает от всех SQL-инъекций, особенно при многобайтовых кодировках.strip_tags()— удаляет теги, но не экранирует вывод. XSS всё ещё возможен через атрибуты.
Пример опасного кода, который до сих пор встречается:
<?php
$id = $_GET['id'];
$id = addslashes($id);
$query = "SELECT * FROM users WHERE id = '$id'";
// Если id = 1 OR 1=1 -- вас взломали
?>
Как надо: Используйте подготовленные запросы (prepared statements). Это единственный надёжный способ защиты от SQL-инъекций.
<?php
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = :id");
$stmt->execute(['id' => $_GET['id']]);
?>
Видите? Никакой санитации для SQL. Просто параметры. База данных сама разберётся, что есть данные, а что — команда.
Ошибка №3: Путаница между валидацией и санитацией
Многие думают: «Я удалю все подозрительные символы, и будет безопасно». Но это ломает логику приложения. Представьте, что пользователь ввёл пароль P@ssw0rd!, а вы удалили символ @ и !. Пароль стал Pssw0rd — и пользователь больше не может войти.
Валидация — проверяет, соответствует ли ввод ожидаемому формату (email, число, URL). Если нет — отклоняем.
Санитация — удаляет или изменяет нежелательные символы, но только когда это действительно нужно (например, для имени файла).
Пример правильной валидации email:
<?php
$email = filter_var($_POST['email'], FILTER_VALIDATE_EMAIL);
if ($email === false) {
// Email невалиден — не сохраняем, не выводим
echo "Введите корректный email";
exit;
}
// Теперь $email — безопасная строка для любых целей
?>
Золотое правило: Если вы ждёте число — проверьте, что это число. Если email — проверьте формат. Всё остальное — отклоняйте.
Ошибка №4: Игнорирование контекста вывода (XSS)
Вы сохранили данные в базу через подготовленные запросы. Ура, SQL-инъекций нет. Но потом вы выводите эти данные на страницу:
<?php
echo "<p>Привет, " . $user['name'] . "!</p>";
// Если $user['name'] = "<script>alert('XSS')</script>"
?>
Бинго! Вы только что подарили хакеру возможность выполнить JavaScript на вашем сайте. Это называется XSS (межсайтовый скриптинг).
Многие думают, что достаточно htmlspecialchars() для всего. Но это не так:
- В HTML-контексте (между тегами) — да,
htmlspecialchars($data, ENT_QUOTES, 'UTF-8'). - В атрибутах HTML — тоже
htmlspecialchars, но с осторожностью. - В JavaScript (внутри
<script>) — нужно экранировать для JS. - В URL —
urlencode(). - В JSON —
json_encode().
Пример правильного вывода:
<?php
// Для HTML
echo "<p>Привет, " . htmlspecialchars($user['name'], ENT_QUOTES, 'UTF-8') . "!</p>";
// Для атрибута
echo '<input value="' . htmlspecialchars($search, ENT_QUOTES, 'UTF-8') . '">';
// Для JavaScript (лучше не использовать, но если надо)
echo "<script>var name = " . json_encode($user['name']) . ";</script>";
?>
Запомните: Экранирование вывода — это не санитация ввода. Это отдельный этап, который происходит в момент отображения данных, а не при их сохранении.
Ошибка №5: Игнорирование глобальных настроек и кодировок
Вы используете htmlspecialchars() с кодировкой по умолчанию? Поздравляю, вы в зоне риска. Если кодировка не указана, PHP использует ISO-8859-1 (или более старую), что может привести к обходу экранирования через многобайтовые символы.
Пример уязвимости:
<?php
// Опасно — кодировка не указана
echo htmlspecialchars($data);
// Безопасно — явно указываем