SQL-инъекции остаются в топ-3 критических уязвимостей веб-приложений согласно OWASP 2024. Каждый день хакеры эксплуатируют эту уязвимость, получая доступ к базам данных тысяч сайтов. В этом руководстве разберем все современные методы защиты PHP-приложений от SQL-атак, от базовых prepared statements до продвинутых техник валидации. Вы получите готовые решения, которые можно применить в своих проектах уже сегодня.
Это вторая часть цикла статей о безопасности PHP. Первая часть посвящена защите от XSS-атак, рекомендую ознакомиться для комплексного понимания безопасности веб-приложений.
Злоумышленник использует оператор UNION для объединения результатов своего запроса с легитимным:
2. Blind SQL Injection (Boolean-based)
Атакующий извлекает данные побитово через true/false ответы:
3. Time-based Blind SQL Injection
Использует временные задержки для извлечения данных:
4. Error-based SQL Injection
Эксплуатирует сообщения об ошибках базы данных:
5. Second-Order SQL Injection
Вредоносные данные сохраняются в БД и выполняются позже:
MySQLi также поддерживает prepared statements, но с другим синтаксисом:
Дополнительный уровень защиты: Помимо WAF, рекомендую настроить Content Security Policy (CSP) для предотвращения XSS-атак, которые часто используются в связке с SQL-инъекциями. Подробное руководство по настройке CSP доступно в статье для начинающих.
В 2023 году хакеры взломали популярный интернет-магазин через уязвимость в функции обновления корзины. Код выглядел так:
Эксплуатация:
Хакеры изменили запрос, установив отрицательную цену товара:
Решение:
API для мобильного приложения имел уязвимость в endpoint получения профиля:
Эксплуатация:
Решение:
Начните с внедрения prepared statements во всех новых проектах, постепенно рефакторьте старый код и обязательно настройте мониторинг подозрительной активности. Современные инструменты и фреймворки делают защиту от SQL инъекций проще, но требуют понимания принципов безопасности для правильного применения.
Это вторая часть цикла статей о безопасности PHP. Первая часть посвящена защите от XSS-атак, рекомендую ознакомиться для комплексного понимания безопасности веб-приложений.
Содержание
- Что такое SQL-инъекция: типы и механизм работы
- 7 проверенных методов защиты от SQL-инъекций
- Примеры уязвимого и безопасного кода
- Реальные кейсы взлома и их предотвращение
- Инструменты для тестирования безопасности
- Чек-лист безопасности разработчика
- Частые вопросы (FAQ)
Что такое SQL-инъекция: типы и механизм работы
SQL-инъекция это метод атаки, при котором злоумышленник внедряет вредоносный SQL-код через пользовательский ввод, получая несанкционированный доступ к базе данных. Эта уязвимость возникает, когда приложение напрямую конкатенирует пользовательские данные с SQL-запросами без должной валидации и экранирования.Основные типы SQL-инъекций
1. Classic SQL Injection (Union-based)Злоумышленник использует оператор UNION для объединения результатов своего запроса с легитимным:
PHP:
// Уязвимый код
$id = $_GET['id'];
$query = "SELECT * FROM users WHERE id = $id";
// Атака: ?id=1 UNION SELECT username, password FROM admin
2. Blind SQL Injection (Boolean-based)
Атакующий извлекает данные побитово через true/false ответы:
PHP:
// Атака: ?id=1 AND SUBSTRING(password,1,1)='a'
// Если страница загружается нормально - первый символ пароля 'a'
Использует временные задержки для извлечения данных:
PHP:
// Атака: ?id=1 AND IF(SUBSTRING(password,1,1)='a', SLEEP(5), 0)
// Если страница грузится 5 секунд - первый символ 'a'
Эксплуатирует сообщения об ошибках базы данных:
PHP:
// Атака провоцирует ошибку с выводом структуры БД
// ?id=1 AND extractvalue(1, concat(0x7e, version()))
Вредоносные данные сохраняются в БД и выполняются позже:
PHP:
// Регистрация пользователя: admin'--
// При следующем запросе происходит инъекция
7 проверенных методов защиты от SQL-инъекций
1. Prepared Statements с PDO (рекомендуемый метод)
PDO (PHP Data Objects) предоставляет унифицированный интерфейс для работы с различными СУБД. Prepared statements полностью разделяют SQL-логику от данных:
PHP:
<?php
// Безопасное подключение к БД
$dsn = 'mysql:host=localhost;dbname=myapp;charset=utf8mb4';
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false, // Важно для настоящих prepared statements
];
try {
$pdo = new PDO($dsn, $username, $password, $options);
} catch (PDOException $e) {
// Логируем ошибку, не показываем пользователю
error_log($e->getMessage());
die('Ошибка подключения к базе данных');
}
// Пример безопасного запроса с именованными параметрами
function getUserById($pdo, $userId) {
$sql = "SELECT id, username, email, created_at
FROM users
WHERE id = :user_id AND status = :status";
$stmt = $pdo->prepare($sql);
$stmt->execute([
':user_id' => $userId,
':status' => 'active'
]);
return $stmt->fetch();
}
// Пример с позиционными параметрами
function searchUsers($pdo, $search, $limit = 10) {
$sql = "SELECT * FROM users
WHERE username LIKE ?
ORDER BY created_at DESC
LIMIT ?";
$stmt = $pdo->prepare($sql);
$stmt->execute(["%$search%", $limit]);
return $stmt->fetchAll();
}
// Массовая вставка данных
function insertMultipleUsers($pdo, $users) {
$sql = "INSERT INTO users (username, email, password_hash)
VALUES (:username, :email, :password)";
$stmt = $pdo->prepare($sql);
foreach ($users as $user) {
$stmt->execute([
':username' => $user['username'],
':email' => $user['email'],
':password' => password_hash($user['password'], PASSWORD_DEFAULT)
]);
}
}
2. Prepared Statements с MySQLi
MySQLi также поддерживает prepared statements, но с другим синтаксисом:
PHP:
<?php
$mysqli = new mysqli('localhost', 'username', 'password', 'database');
if ($mysqli->connect_error) {
error_log($mysqli->connect_error);
die('Ошибка подключения');
}
// Установка кодировки - критично для безопасности
$mysqli->set_charset('utf8mb4');
// Пример безопасного SELECT запроса
function getUserByEmail($mysqli, $email) {
$sql = "SELECT id, username, created_at FROM users WHERE email = ?";
if ($stmt = $mysqli->prepare($sql)) {
// 's' означает string, 'i' - integer, 'd' - double, 'b' - blob
$stmt->bind_param("s", $email);
$stmt->execute();
$result = $stmt->get_result();
$user = $result->fetch_assoc();
$stmt->close();
return $user;
}
return null;
}
// Пример с множественными параметрами
function updateUserProfile($mysqli, $userId, $username, $bio) {
$sql = "UPDATE users SET username = ?, bio = ? WHERE id = ?";
if ($stmt = $mysqli->prepare($sql)) {
$stmt->bind_param("ssi", $username, $bio, $userId);
$success = $stmt->execute();
$stmt->close();
return $success;
}
return false;
}
// Пример безопасной процедуры с транзакцией
function transferMoney($mysqli, $fromId, $toId, $amount) {
$mysqli->autocommit(false);
try {
// Снимаем деньги
$sql1 = "UPDATE accounts SET balance = balance - ? WHERE user_id = ? AND balance >= ?";
$stmt1 = $mysqli->prepare($sql1);
$stmt1->bind_param("did", $amount, $fromId, $amount);
$stmt1->execute();
if ($stmt1->affected_rows === 0) {
throw new Exception("Недостаточно средств");
}
// Зачисляем деньги
$sql2 = "UPDATE accounts SET balance = balance + ? WHERE user_id = ?";
$stmt2 = $mysqli->prepare($sql2);
$stmt2->bind_param("di", $amount, $toId);
$stmt2->execute();
$mysqli->commit();
return true;
} catch (Exception $e) {
$mysqli->rollback();
error_log($e->getMessage());
return false;
}
}
3. Валидация и санитизация входных данных
Валидация должна происходить на нескольких уровнях:
PHP:
<?php
// Класс для валидации различных типов данных
class InputValidator {
// Валидация целых чисел
public static function validateInt($input, $min = null, $max = null) {
$options = ['options' => []];
if ($min !== null) {
$options['options']['min_range'] = $min;
}
if ($max !== null) {
$options['options']['max_range'] = $max;
}
return filter_var($input, FILTER_VALIDATE_INT, $options);
}
// Валидация email
public static function validateEmail($email) {
$email = filter_var($email, FILTER_SANITIZE_EMAIL);
return filter_var($email, FILTER_VALIDATE_EMAIL) ? $email : false;
}
// Валидация строк с whitelist символов
public static function validateString($input, $pattern = '/^[a-zA-Z0-9_\-]+$/') {
$input = trim($input);
if (preg_match($pattern, $input)) {
return $input;
}
return false;
}
// Валидация UUID
public static function validateUUID($uuid) {
$pattern = '/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i';
return preg_match($pattern, $uuid) ? $uuid : false;
}
// Валидация дат
public static function validateDate($date, $format = 'Y-m-d') {
$d = DateTime::createFromFormat($format, $date);
return $d && $d->format($format) === $date ? $date : false;
}
// Комплексная валидация для формы
public static function validateUserForm($data) {
$errors = [];
$clean = [];
// Username
if (empty($data['username'])) {
$errors['username'] = 'Username обязателен';
} else {
$username = self::validateString($data['username'], '/^[a-zA-Z0-9_]{3,20}$/');
if (!$username) {
$errors['username'] = 'Username может содержать только буквы, цифры и _ (3-20 символов)';
} else {
$clean['username'] = $username;
}
}
// Email
if (empty($data['email'])) {
$errors['email'] = 'Email обязателен';
} else {
$email = self::validateEmail($data['email']);
if (!$email) {
$errors['email'] = 'Некорректный email';
} else {
$clean['email'] = $email;
}
}
// Age
if (isset($data['age'])) {
$age = self::validateInt($data['age'], 13, 120);
if ($age === false) {
$errors['age'] = 'Возраст должен быть от 13 до 120 лет';
} else {
$clean['age'] = $age;
}
}
return [
'valid' => empty($errors),
'data' => $clean,
'errors' => $errors
];
}
}
// Использование валидатора
$formData = $_POST;
$validation = InputValidator::validateUserForm($formData);
if ($validation['valid']) {
// Используем очищенные данные
$userData = $validation['data'];
// Сохраняем в БД через prepared statements
} else {
// Показываем ошибки
foreach ($validation['errors'] as $field => $error) {
echo "Ошибка в поле $field: $error<br>";
}
}
4. Использование ORM (Eloquent, Doctrine)
ORM автоматически защищают от SQL-инъекций:
PHP:
<?php
// Пример с Laravel Eloquent
use App\Models\User;
use Illuminate\Support\Facades\DB;
// Безопасные запросы через модель
$user = User::where('email', $email)->first();
$users = User::where('status', 'active')
->where('created_at', '>=', now()->subDays(30))
->orderBy('name')
->paginate(10);
// Query Builder тоже безопасен
$users = DB::table('users')
->where('votes', '>', 100)
->orWhere('name', 'like', '%john%')
->get();
// Raw запросы с параметрами
$results = DB::select('SELECT * FROM users WHERE id = ?', [$id]);
$affected = DB::update('UPDATE users SET votes = 100 WHERE name = ?', [$name]);
// Пример с Doctrine ORM
use Doctrine\ORM\EntityManager;
class UserRepository {
private $em;
public function __construct(EntityManager $em) {
$this->em = $em;
}
public function findActiveUsers($limit = 10) {
$qb = $this->em->createQueryBuilder();
return $qb->select('u')
->from('App\Entity\User', 'u')
->where('u.status = :status')
->setParameter('status', 'active')
->orderBy('u.createdAt', 'DESC')
->setMaxResults($limit)
->getQuery()
->getResult();
}
public function searchUsers($term) {
$query = $this->em->createQuery(
'SELECT u FROM App\Entity\User u
WHERE u.username LIKE :term OR u.email LIKE :term'
);
$query->setParameter('term', '%' . $term . '%');
return $query->getResult();
}
}
5. Хранимые процедуры
Хранимые процедуры добавляют дополнительный уровень защиты:
PHP:
<?php
// Создание хранимой процедуры в MySQL
$createProcedure = "
CREATE PROCEDURE GetUserById(IN userId INT)
BEGIN
SELECT id, username, email, created_at
FROM users
WHERE id = userId AND deleted_at IS NULL;
END;
";
// Вызов хранимой процедуры из PHP
function callStoredProcedure($pdo, $userId) {
$stmt = $pdo->prepare("CALL GetUserById(:userId)");
$stmt->bindParam(':userId', $userId, PDO::PARAM_INT);
$stmt->execute();
return $stmt->fetch();
}
// Хранимая процедура с OUTPUT параметром
$createProcedure2 = "
CREATE PROCEDURE AuthenticateUser(
IN userEmail VARCHAR(255),
IN userPassword VARCHAR(255),
OUT userId INT,
OUT authResult BOOLEAN
)
BEGIN
DECLARE storedPassword VARCHAR(255);
SELECT id, password_hash INTO userId, storedPassword
FROM users
WHERE email = userEmail
LIMIT 1;
IF userId IS NOT NULL AND PASSWORD_VERIFY(userPassword, storedPassword) THEN
SET authResult = TRUE;
ELSE
SET authResult = FALSE;
SET userId = NULL;
END IF;
END;
";
// Использование процедуры с OUTPUT
function authenticateUser($mysqli, $email, $password) {
$stmt = $mysqli->prepare("CALL AuthenticateUser(?, ?, @userId, @authResult)");
$stmt->bind_param("ss", $email, $password);
$stmt->execute();
$select = $mysqli->query("SELECT @userId, @authResult");
$result = $select->fetch_assoc();
return [
'authenticated' => (bool)$result['@authResult'],
'user_id' => $result['@userId']
];
}
6. Принцип наименьших привилегий
Настройка правильных прав доступа к БД:
PHP:
<?php
// Создание разных пользователей БД для разных операций
class DatabaseConnectionFactory {
private static $connections = [];
// Пользователь только для чтения
public static function getReadConnection() {
if (!isset(self::$connections['read'])) {
self::$connections['read'] = new PDO(
'mysql:host=localhost;dbname=myapp;charset=utf8mb4',
'app_reader', // Пользователь с правами только SELECT
'read_password',
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
}
return self::$connections['read'];
}
// Пользователь для записи
public static function getWriteConnection() {
if (!isset(self::$connections['write'])) {
self::$connections['write'] = new PDO(
'mysql:host=localhost;dbname=myapp;charset=utf8mb4',
'app_writer', // Пользователь с правами INSERT, UPDATE, DELETE
'write_password',
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
}
return self::$connections['write'];
}
// Административный пользователь (используется редко)
public static function getAdminConnection() {
// Дополнительная проверка прав
if (!self::isAdminContext()) {
throw new Exception("Доступ запрещен");
}
if (!isset(self::$connections['admin'])) {
self::$connections['admin'] = new PDO(
'mysql:host=localhost;dbname=myapp;charset=utf8mb4',
'app_admin',
'admin_password',
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
}
return self::$connections['admin'];
}
private static function isAdminContext() {
// Проверка, что код выполняется в административном контексте
return defined('ADMIN_CONTEXT') && ADMIN_CONTEXT === true;
}
}
// SQL для создания пользователей с ограниченными правами
/*
-- Пользователь для чтения
CREATE USER 'app_reader'@'localhost' IDENTIFIED BY 'read_password';
GRANT SELECT ON myapp.* TO 'app_reader'@'localhost';
-- Пользователь для записи
CREATE USER 'app_writer'@'localhost' IDENTIFIED BY 'write_password';
GRANT SELECT, INSERT, UPDATE, DELETE ON myapp.* TO 'app_writer'@'localhost';
-- Административный пользователь
CREATE USER 'app_admin'@'localhost' IDENTIFIED BY 'admin_password';
GRANT ALL PRIVILEGES ON myapp.* TO 'app_admin'@'localhost';
FLUSH PRIVILEGES;
*/
7. Web Application Firewall (WAF) и мониторинг
Реализация простого WAF на уровне приложения:
PHP:
<?php
class SimpleSQLFirewall {
private $suspiciousPatterns = [
'/union\s+select/i',
'/select.*from.*information_schema/i',
'/select.*from.*mysql\./i',
'/into\s+outfile/i',
'/load_file/i',
'/benchmark\s*\(/i',
'/sleep\s*\(/i',
'/waitfor\s+delay/i',
'/(drop|alter|create|truncate)\s+table/i',
'/exec\s*\(/i',
'/xp_cmdshell/i',
'/script\s*>/i',
'/javascript:/i',
'/onerror\s*=/i',
'/onload\s*=/i',
'/eval\s*\(/i',
'/base64_decode/i'
];
private $logger;
public function __construct($logger = null) {
$this->logger = $logger;
}
public function checkRequest($input) {
$threats = [];
// Проверяем все входящие данные
$checkData = is_array($input) ? json_encode($input) : (string)$input;
foreach ($this->suspiciousPatterns as $pattern) {
if (preg_match($pattern, $checkData)) {
$threats[] = $pattern;
}
}
if (!empty($threats)) {
$this->logThreat($threats, $input);
return false;
}
return true;
}
private function logThreat($threats, $input) {
$logData = [
'timestamp' => date('Y-m-d H:i:s'),
'ip' => $_SERVER['REMOTE_ADDR'] ?? 'unknown',
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'unknown',
'threats' => $threats,
'input' => $input,
'uri' => $_SERVER['REQUEST_URI'] ?? 'unknown'
];
// Логирование в файл или SIEM систему
if ($this->logger) {
$this->logger->warning('SQL Injection attempt detected', $logData);
} else {
error_log('SQL Injection attempt: ' . json_encode($logData));
}
// Можно добавить блокировку IP после N попыток
$this->checkForRepeatedAttempts($_SERVER['REMOTE_ADDR']);
}
private function checkForRepeatedAttempts($ip) {
// Реализация rate limiting и блокировки
$cacheKey = "sql_attempts_$ip";
$attempts = apcu_fetch($cacheKey) ?: 0;
$attempts++;
apcu_store($cacheKey, $attempts, 3600); // Хранить 1 час
if ($attempts > 5) {
// Блокировка IP или отправка алерта
$this->blockIp($ip);
}
}
private function blockIp($ip) {
// Добавление в blacklist
file_put_contents('/var/www/blacklist.txt', "$ip\n", FILE_APPEND | LOCK_EX);
// Или использование iptables
// exec("iptables -A INPUT -s $ip -j DROP");
}
}
// Использование
$firewall = new SimpleSQLFirewall();
// Проверка всех входящих данных
$allInput = array_merge($_GET, $_POST, $_COOKIE);
if (!$firewall->checkRequest($allInput)) {
http_response_code(403);
die('Подозрительная активность заблокирована');
}

Примеры уязвимого и безопасного кода
Пример 1: Аутентификация пользователя
PHP:
<?php
// ❌ УЯЗВИМЫЙ КОД
function authenticateUserVulnerable($username, $password) {
global $mysqli;
$query = "SELECT * FROM users WHERE username = '$username' AND password = '$password'";
$result = $mysqli->query($query);
if ($result->num_rows > 0) {
return $result->fetch_assoc();
}
return false;
}
// Атака: username = admin' --
// Результат: вход без пароля
// ✅ БЕЗОПАСНЫЙ КОД
function authenticateUserSecure($pdo, $username, $password) {
$sql = "SELECT id, username, password_hash FROM users WHERE username = :username LIMIT 1";
$stmt = $pdo->prepare($sql);
$stmt->execute([':username' => $username]);
$user = $stmt->fetch();
if ($user && password_verify($password, $user['password_hash'])) {
// Удаляем хэш пароля из результата
unset($user['password_hash']);
return $user;
}
return false;
}
Пример 2: Поиск по сайту
PHP:
<?php
// ❌ УЯЗВИМЫЙ КОД
function searchProductsVulnerable($search) {
global $mysqli;
$query = "SELECT * FROM products WHERE name LIKE '%$search%' OR description LIKE '%$search%'";
return $mysqli->query($query);
}
// Атака: search = %' UNION SELECT * FROM credit_cards --
// ✅ БЕЗОПАСНЫЙ КОД
function searchProductsSecure($pdo, $search, $category = null, $limit = 20) {
// Базовый запрос
$sql = "SELECT id, name, price, description, image_url
FROM products
WHERE (name LIKE :search OR description LIKE :search)";
$params = [':search' => "%$search%"];
// Динамическое построение запроса
if ($category) {
$sql .= " AND category_id = :category";
$params[':category'] = $category;
}
$sql .= " ORDER BY relevance DESC LIMIT :limit";
$stmt = $pdo->prepare($sql);
// Bind параметров с указанием типа
foreach ($params as $key => $value) {
if ($key === ':limit') {
$stmt->bindValue($key, $value, PDO::PARAM_INT);
} else {
$stmt->bindValue($key, $value);
}
}
$stmt->execute();
return $stmt->fetchAll();
}
Пример 3: Динамическая сортировка
PHP:
<?php
// ❌ УЯЗВИМЫЙ КОД
function getUsersVulnerable($sort) {
global $mysqli;
$query = "SELECT * FROM users ORDER BY $sort";
return $mysqli->query($query);
}
// Атака: sort = (SELECT password FROM admin)
// ✅ БЕЗОПАСНЫЙ КОД
function getUsersSecure($pdo, $sort = 'created_at', $direction = 'DESC') {
// Whitelist разрешенных полей для сортировки
$allowedSorts = [
'created_at' => 'created_at',
'username' => 'username',
'email' => 'email',
'last_login' => 'last_login'
];
$allowedDirections = ['ASC', 'DESC'];
// Валидация параметров сортировки
$sortField = $allowedSorts[$sort] ?? 'created_at';
$sortDirection = in_array(strtoupper($direction), $allowedDirections)
? strtoupper($direction)
: 'DESC';
// Безопасный запрос
$sql = "SELECT id, username, email, created_at
FROM users
WHERE status = :status
ORDER BY {$sortField} {$sortDirection}";
$stmt = $pdo->prepare($sql);
$stmt->execute([':status' => 'active']);
return $stmt->fetchAll();
}
Реальные кейсы взлома и их предотвращение
Кейс 1: Взлом интернет-магазина через корзину
Сценарий атаки:В 2023 году хакеры взломали популярный интернет-магазин через уязвимость в функции обновления корзины. Код выглядел так:
PHP:
// Уязвимый код магазина
$product_id = $_POST['product_id'];
$quantity = $_POST['quantity'];
$user_id = $_SESSION['user_id'];
$query = "UPDATE cart SET quantity = $quantity WHERE product_id = $product_id AND user_id = $user_id";
mysqli_query($connection, $query);
Хакеры изменили запрос, установив отрицательную цену товара:
Код:
POST: quantity = 1, price = -1000 WHERE product_id = 1 OR 1=1 --
PHP:
<?php
// Безопасная реализация корзины
class SecureCart {
private $pdo;
public function updateQuantity($userId, $productId, $quantity) {
// Валидация входных данных
$quantity = filter_var($quantity, FILTER_VALIDATE_INT, [
'options' => ['min_range' => 0, 'max_range' => 99]
]);
if ($quantity === false) {
throw new InvalidArgumentException('Некорректное количество товара');
}
// Проверка существования товара
$checkSql = "SELECT id, price, stock FROM products WHERE id = :product_id";
$checkStmt = $this->pdo->prepare($checkSql);
$checkStmt->execute([':product_id' => $productId]);
$product = $checkStmt->fetch();
if (!$product) {
throw new Exception('Товар не найден');
}
if ($product['stock'] < $quantity) {
throw new Exception('Недостаточно товара на складе');
}
// Обновление корзины
$updateSql = "INSERT INTO cart (user_id, product_id, quantity, price)
VALUES (:user_id, :product_id, :quantity, :price)
ON DUPLICATE KEY UPDATE
quantity = :quantity,
price = :price,
updated_at = NOW()";
$stmt = $this->pdo->prepare($updateSql);
return $stmt->execute([
':user_id' => $userId,
':product_id' => $productId,
':quantity' => $quantity,
':price' => $product['price']
]);
}
}
Кейс 2: Утечка базы данных через API endpoint
Сценарий атаки:API для мобильного приложения имел уязвимость в endpoint получения профиля:
PHP:
// Уязвимый API endpoint
Route::get('/api/user/{id}', function($id) {
$user = DB::select("SELECT * FROM users WHERE id = $id");
return response()->json($user);
});
Код:
GET /api/user/1 UNION SELECT credit_card_number, cvv, expiry FROM payment_methods
PHP:
<?php
// Безопасный API endpoint с дополнительными проверками
class UserApiController {
public function getProfile(Request $request, $id) {
// Валидация ID
if (!is_numeric($id) || $id < 1) {
return response()->json(['error' => 'Invalid user ID'], 400);
}
// Проверка авторизации
if (!$this->canAccessProfile($request->user(), $id)) {
return response()->json(['error' => 'Access denied'], 403);
}
// Безопасный запрос с ограниченными полями
$user = User::select(['id', 'username', 'email', 'created_at'])
->where('id', $id)
->where('status', 'active')
->first();
if (!$user) {
return response()->json(['error' => 'User not found'], 404);
}
// Логирование доступа
$this->logAccess($request->user()->id, $id);
// Добавление rate limiting заголовков
return response()->json($user)
->header('X-RateLimit-Limit', 60)
->header('X-RateLimit-Remaining', $this->getRemainingRequests());
}
private function canAccessProfile($currentUser, $targetId) {
// Пользователь может видеть только свой профиль
// или если он админ
return $currentUser->id == $targetId || $currentUser->isAdmin();
}
private function logAccess($accessorId, $targetId) {
DB::table('api_access_logs')->insert([
'accessor_id' => $accessorId,
'target_id' => $targetId,
'endpoint' => 'user_profile',
'ip' => request()->ip(),
'created_at' => now()
]);
}
}
Инструменты для тестирования безопасности
1. SQLMap - автоматическое тестирование
Bash:
# Базовое сканирование
sqlmap -u "http://example.com/page.php?id=1" --batch
# Сканирование с cookie
sqlmap -u "http://example.com/page.php?id=1" --cookie="PHPSESSID=abc123"
# Сканирование POST запросов
sqlmap -u "http://example.com/login.php" --data="username=admin&password=test"
# Получение структуры БД
sqlmap -u "http://example.com/page.php?id=1" --tables
# Дамп конкретной таблицы
sqlmap -u "http://example.com/page.php?id=1" -D database -T users --dump
2. Тестирование вручную
PHP:
<?php
// Скрипт для self-тестирования
class SQLInjectionTester {
private $testPayloads = [
"1' OR '1'='1",
"1' OR '1'='1' --",
"1' OR '1'='1' /*",
"' OR 1=1--",
"1 UNION SELECT NULL--",
"1' AND 1=CONVERT(int, (SELECT @@version))--",
"1' AND SLEEP(5)--",
"1'; DROP TABLE users--",
"1' AND (SELECT * FROM (SELECT(SLEEP(5)))a)--"
];
public function testEndpoint($url, $parameter) {
$results = [];
foreach ($this->testPayloads as $payload) {
$testUrl = $url . "?$parameter=" . urlencode($payload);
$startTime = microtime(true);
$response = @file_get_contents($testUrl);
$responseTime = microtime(true) - $startTime;
$results[] = [
'payload' => $payload,
'response_time' => $responseTime,
'error_detected' => $this->checkForSQLErrors($response),
'status_code' => $http_response_header[0] ?? 'unknown'
];
}
return $results;
}
private function checkForSQLErrors($response) {
$errorPatterns = [
'/SQL syntax/',
'/mysql_fetch/',
'/Warning.*mysql/',
'/MySQLSyntaxErrorException/',
'/PostgreSQL/',
'/valid MySQL result/',
'/mssql_/',
'/SQLServer JDBC Driver/',
'/Oracle error/',
'/Oracle driver/'
];
foreach ($errorPatterns as $pattern) {
if (preg_match($pattern, $response)) {
return true;
}
}
return false;
}
}
3. Burp Suite настройка для PHP
Конфигурация Burp Suite для тестирования PHP приложений:- Scanner настройки:
- Включить "SQL Injection" в активных проверках
- Установить insertion points на все параметры
- Использовать грамотные payloads для PHP/MySQL
- Intruder настройки:
- Payload type: Simple list
- Добавить специфичные для PHP payloads
- Настроить grep-match для PHP ошибок
- Расширения для PHP:
- SQLiPy Sqlmap Integration
- PHP Object Injection Check
- Autorize для проверки авторизации
Чек-лист безопасности разработчика
Обязательные проверки перед деплоем
- Все SQL запросы используют prepared statements
- PDO или MySQLi с параметризацией
- Никаких прямых конкатенаций с пользовательским вводом
- Валидация всех входных данных
- Whitelist подход для разрешенных символов
- Проверка типов данных
- Ограничение длины строк
- Правильная обработка ошибок
- Отключен вывод ошибок в продакшене
- Логирование ошибок в файлы
- Общие сообщения для пользователей
- Настроены права доступа к БД
- Разные пользователи для чтения/записи
- Минимальные необходимые привилегии
- Отключен FILE privilege
- Включены механизмы защиты
- WAF или правила фильтрации
- Rate limiting для API
- Мониторинг подозрительной активности
- Регулярные обновления
- PHP версии 8.0+
- Актуальные версии MySQL/PostgreSQL
- Обновленные библиотеки и фреймворки
- Тестирование безопасности
- Автоматические тесты на SQL инъекции
- Ручное тестирование критических endpoint'ов
- Регулярный аудит кода
Конфигурация php.ini для безопасности
INI:
; Отключение опасных функций
disable_functions = exec,passthru,shell_exec,system,proc_open,popen,curl_exec,curl_multi_exec,parse_ini_file,show_source
; Отключение отображения ошибок
display_errors = Off
log_errors = On
error_log = /var/log/php/error.log
; Ограничения
max_execution_time = 30
max_input_time = 60
memory_limit = 128M
post_max_size = 8M
upload_max_filesize = 2M
; Безопасность сессий
session.cookie_httponly = 1
session.cookie_secure = 1
session.use_only_cookies = 1
session.use_strict_mode = 1
; Другие настройки безопасности
expose_php = Off
allow_url_fopen = Off
allow_url_include = Off
Частые вопросы (FAQ)
Что такое SQL инъекция простыми словами?
SQL инъекция это уязвимость, позволяющая злоумышленнику выполнять произвольные SQL команды в базе данных через веб-приложение. Представьте, что ваше приложение это охранник, который проверяет пропуска (SQL запросы) на входе в здание (база данных). SQL инъекция это когда злоумышленник подделывает пропуск так, что охранник не только пускает его, но и дает ключи от всех помещений.Как проверить свой сайт на SQL инъекции?
Базовая проверка включает:- Добавление одинарной кавычки (') в параметры URL
- Использование SQLMap для автоматического сканирования
- Проверка форм с payloads типа
' OR '1'='1
- Мониторинг логов на SQL ошибки
- Использование онлайн сканеров безопасности
Достаточно ли использовать mysqli_real_escape_string?
Нет,mysqli_real_escape_string()
не является достаточной защитой. Эта функция экранирует специальные символы, но не защищает от всех типов инъекций, особенно в числовых полях. Всегда используйте prepared statements как основной метод защиты.Какая разница между PDO и MySQLi для защиты?
Оба расширения поддерживают prepared statements и при правильном использовании одинаково безопасны. PDO имеет преимущества: поддержка разных СУБД, именованные параметры, более удобный API. MySQLi работает только с MySQL, но может быть быстрее для специфичных MySQL операций.Можно ли делать динамические запросы безопасно?
Да, но с ограничениями:- Используйте whitelist для имен таблиц и колонок
- Параметризируйте все значения через prepared statements
- Никогда не конкатенируйте пользовательский ввод напрямую
- Валидируйте все входные данные
Защищают ли ORM от SQL инъекций автоматически?
Современные ORM (Eloquent, Doctrine) защищают от SQL инъекций при использовании их стандартных методов. Однако, raw queries и неправильное использование могут создать уязвимости. Всегда параметризируйте raw запросы и избегайте динамического построения SQL.Как часто нужно проводить аудит безопасности?
Рекомендуемая периодичность:- Перед каждым major релизом
- После значительных изменений в коде работы с БД
- Минимум раз в квартал для критических систем
- При обнаружении новых типов атак
Как защититься от комплексных атак (SQL + CSRF + XSS)?
Современные атаки часто используют комбинацию уязвимостей. Например, через SQL-инъекцию злоумышленник может внедрить XSS-код в базу данных, который затем будет выполнен у всех пользователей (Stored XSS). Для полной защиты необходимо:- Использовать prepared statements для всех SQL запросов
- Экранировать вывод данных из БД
- Внедрить CSRF-токены для защиты от межсайтовой подделки запросов
Что делать при обнаружении SQL инъекции в продакшене?
План действий:- Немедленно изолировать уязвимый функционал
- Проанализировать логи на предмет эксплуатации
- Применить временный фикс (WAF правило)
- Разработать и протестировать постоянное решение
- Провести аудит на похожие уязвимости
- Уведомить пользователей если были скомпрометированы данные
Заключение
Защита от SQL инъекций требует комплексного подхода: от правильного написания кода до настройки инфраструктуры. Используйте prepared statements как основу защиты, добавляйте валидацию данных, применяйте принцип наименьших привилегий и регулярно тестируйте безопасность. Помните, что безопасность это не разовая задача, а постоянный процесс.Начните с внедрения prepared statements во всех новых проектах, постепенно рефакторьте старый код и обязательно настройте мониторинг подозрительной активности. Современные инструменты и фреймворки делают защиту от SQL инъекций проще, но требуют понимания принципов безопасности для правильного применения.
Полезные ресурсы для углубленного изучения
-
Ссылка скрыта от гостей
-
Ссылка скрыта от гостей
-
Ссылка скрыта от гостей
-
Ссылка скрыта от гостей
-
Ссылка скрыта от гостей
- Практика: root-me.org, alexbers.com/sql/, dvwa.co.uk
-
Ссылка скрыта от гостей
-
Ссылка скрыта от гостей
Вложения
Последнее редактирование модератором: