🧱 Суть
SQL Injection – это уязвимость, при которой пользователь может изменить структуру SQL-запроса через входные данные.
Проблема возникает, когда данные пользователя напрямую вставляются в SQL.
Пример:
$id = $_GET['id'];
$sql = "
SELECT *
FROM users
WHERE id = $id
";Запрос:
?id=5
превращается в:
SELECT *
FROM users
WHERE id = 5Но пользователь может отправить:
?id=5 OR 1=1
И получить:
SELECT *
FROM users
WHERE id = 5 OR 1=1Теперь условие всегда истинно.
📦 Проблема
1. Склейка SQL-строк опасна
Плохо:
$email = $_POST['email'];
$sql = "
SELECT *
FROM users
WHERE email = '$email'
";Если пользователь отправит:
' OR '1'='1Получится:
SELECT *
FROM users
WHERE email = '' OR '1'='1'Запрос вернёт все записи.
2. Экранирование строк – плохое решение
Иногда пытаются делать:
mysqli_real_escape_string();или вручную заменять символы.
Проблемы:
- легко ошибиться
- нужно помнить про каждый тип данных
- сложнее поддерживать
Правильное решение:
отделять SQL-код от пользовательских данных
3. Любой внешний ввод считается опасным
Источники риска:
$_GET$_POST$_COOKIE- JSON API
- файлы
- HTTP-заголовки
Нельзя доверять данным только потому, что они пришли от клиента.
✅ Решение
Использовать подготовленные запросы (Prepared Statements).
Идея:
- SQL отправляется отдельно
- данные передаются отдельно
- база сама правильно обрабатывает значения
Схема:
SQL-шаблон
+
Параметры
↓
Безопасный запрос
💻 Код
Prepared Statements в PDO
Вместо:
$sql = "
SELECT *
FROM users
WHERE id = $id
";Используем:
$stmt = $pdo->prepare(
"
SELECT *
FROM users
WHERE id = ?
"
);
$stmt->execute([$id]);Теперь:
$id = "5 OR 1=1";будет восприниматься как обычное значение:
WHERE id = '5 OR 1=1'а не как часть SQL-кода.
Позиционные параметры
Самый простой вариант:
$stmt = $pdo->prepare(
"
SELECT *
FROM users
WHERE id = ?
"
);
$stmt->execute([
15
]);? заменяется значением из массива.
Пример нескольких параметров:
$stmt = $pdo->prepare(
"
SELECT *
FROM users
WHERE email = ?
AND status = ?
"
);
$stmt->execute([
$email,
'active'
]);Именованные параметры
Более читаемый вариант:
$stmt = $pdo->prepare(
"
SELECT *
FROM users
WHERE email = :email
"
);
$stmt->execute([
'email' => $email
]);Плюсы:
- легче читать
- меньше ошибок
- удобно при большом количестве параметров
INSERT через Prepared Statement
Опасно:
$sql = "
INSERT INTO users(name)
VALUES ('$name')
";Безопасно:
$stmt = $pdo->prepare(
"
INSERT INTO users(name)
VALUES (:name)
"
);
$stmt->execute([
'name' => $name
]);UPDATE через Prepared Statement
$stmt = $pdo->prepare(
"
UPDATE users
SET name = :name
WHERE id = :id
"
);
$stmt->execute([
'name' => $name,
'id' => $id
]);DELETE через Prepared Statement
$stmt = $pdo->prepare(
"
DELETE FROM users
WHERE id = :id
"
);
$stmt->execute([
'id' => $id
]);Почему Prepared Statements защищают от SQL Injection
Обычный запрос:
SQL + данные
↓
одна строка
↓
База выполняет код
Проблема:
данные могут стать кодом
Prepared Statement:
SQL
↓
подготовка
данные
↓
параметры
объединение внутри БД
Теперь:
данные остаются данными
Типы параметров
Можно указать тип вручную:
$stmt->bindValue(
':id',
$id,
PDO::PARAM_INT
);Пример:
$stmt = $pdo->prepare(
"
SELECT *
FROM users
WHERE id = :id
"
);
$stmt->bindValue(
':id',
10,
PDO::PARAM_INT
);
$stmt->execute();Основные типы:
| Тип | Константа |
|---|---|
| строка | PDO::PARAM_STR |
| число | PDO::PARAM_INT |
| boolean | PDO::PARAM_BOOL |
| NULL | PDO::PARAM_NULL |
Что Prepared Statements НЕ защищают
Подготовленные запросы защищают значения, но не структуру SQL.
Например:
$order = $_GET['sort'];
$sql = "
SELECT *
FROM users
ORDER BY $order
";Так делать нельзя.
Параметры нельзя использовать для:
- имён таблиц
- имён колонок
- SQL-ключевых слов
Плохо:
ORDER BY ?Правильно:
Использовать whitelist:
$allowed = [
'name',
'created_at'
];
if (
in_array($sort, $allowed)
) {
$sql = "
SELECT *
FROM users
ORDER BY $sort
";
}Prepared Statements и ORM
Современные ORM обычно используют тот же принцип.
Например:
User::where(
'id',
$id
)->first();Внутри ORM создаёт:
WHERE id = ?и передаёт параметр отдельно.
Дополнительная защита
Prepared Statements – основа, но нужны ещё:
Валидация данных
Проверять:
- формат
- длину
- диапазон
Пример:
if (!is_numeric($id)) {
exit();
}Минимальные права пользователя БД
Не стоит подключаться к БД под:
root
Лучше:
app_user
с ограниченными правами.
Обработка ошибок
Не показывать пользователю:
echo $exception->getMessage();Потому что там может быть:
SQL syntax error
Table users doesn't exist
Лучше:
log_error($exception);
echo "Ошибка сервера";Частые ошибки
Склеивать SQL строки
Плохо:
$sql =
"SELECT * FROM users WHERE id="
. $id;Использовать подготовленные запросы только иногда
Плохо:
SELECT * FROM users WHERE id=?но:
DELETE FROM users WHERE id=$idСчитать, что проверка клиента достаточна
Плохо:
<input maxlength="20">Это можно обойти.
Проверка должна быть на сервере.
Хранить пароли напрямую
Плохо:
INSERT password='123456'Лучше:
password_hash(
$password,
PASSWORD_DEFAULT
);🎯 Вывод
- SQL Injection возникает, когда данные пользователя попадают прямо в SQL-код
- нельзя собирать SQL через конкатенацию строк
- Prepared Statements отделяют SQL от данных
- в PDO используются
prepare()иexecute() - параметры можно передавать через
?или именованные:name - подготовленные запросы защищают значения, но не имена таблиц и колонок
- дополнительно нужна валидация входных данных
- безопасность должна быть на стороне сервера