Статья Защита от SQL-инъекций в PHP: Полное руководство 2025 с примерами кода

SQL-инъекции остаются в топ-3 критических уязвимостей веб-приложений согласно OWASP 2024. Каждый день хакеры эксплуатируют эту уязвимость, получая доступ к базам данных тысяч сайтов. В этом руководстве разберем все современные методы защиты PHP-приложений от SQL-атак, от базовых prepared statements до продвинутых техник валидации. Вы получите готовые решения, которые можно применить в своих проектах уже сегодня.

Это вторая часть цикла статей о безопасности PHP. Первая часть посвящена защите от XSS-атак, рекомендую ознакомиться для комплексного понимания безопасности веб-приложений.

Содержание​

  1. Что такое SQL-инъекция: типы и механизм работы
  2. 7 проверенных методов защиты от SQL-инъекций
  3. Примеры уязвимого и безопасного кода
  4. Реальные кейсы взлома и их предотвращение
  5. Инструменты для тестирования безопасности
  6. Чек-лист безопасности разработчика
  7. Частые вопросы (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
SQL injection UNION атака - демонстрация эксплуатации уязвимости в PHP

2. Blind SQL Injection (Boolean-based)
Атакующий извлекает данные побитово через true/false ответы:
PHP:
// Атака: ?id=1 AND SUBSTRING(password,1,1)='a'
// Если страница загружается нормально - первый символ пароля 'a'
3. Time-based Blind SQL Injection
Использует временные задержки для извлечения данных:
PHP:
// Атака: ?id=1 AND IF(SUBSTRING(password,1,1)='a', SLEEP(5), 0)
// Если страница грузится 5 секунд - первый символ 'a'
4. Error-based SQL Injection
Эксплуатирует сообщения об ошибках базы данных:
PHP:
// Атака провоцирует ошибку с выводом структуры БД
// ?id=1 AND extractvalue(1, concat(0x7e, version()))
5. Second-Order SQL Injection
Вредоносные данные сохраняются в БД и выполняются позже:
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)
        ]);
    }
}
PDO prepared statements защита от SQL injection - безопасный код PHP

2. Prepared Statements с MySQLi​

MySQLi параметризованные запросы - защита от SQL инъекций в PHP

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>";
    }
}
Валидация входных данных PHP - фильтрация от SQL injection атак

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('Подозрительная активность заблокирована');
}
💡 Дополнительный уровень защиты: Помимо WAF, рекомендую настроить Content Security Policy (CSP) для предотвращения XSS-атак, которые часто используются в связке с SQL-инъекциями. Подробное руководство по настройке CSP доступно в статье для начинающих.

Примеры уязвимого и безопасного кода​

Обход аутентификации через SQL injection - пример успешной атаки

Пример 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 - автоматическое тестирование​

SQLMap сканирование на SQL injection - определение количества полей

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 приложений:
  1. Scanner настройки:
    • Включить "SQL Injection" в активных проверках
    • Установить insertion points на все параметры
    • Использовать грамотные payloads для PHP/MySQL
  2. Intruder настройки:
    • Payload type: Simple list
    • Добавить специфичные для PHP payloads
    • Настроить grep-match для PHP ошибок
  3. Расширения для 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 инъекции?​

Базовая проверка включает:
  1. Добавление одинарной кавычки (') в параметры URL
  2. Использование SQLMap для автоматического сканирования
  3. Проверка форм с payloads типа ' OR '1'='1
  4. Мониторинг логов на SQL ошибки
  5. Использование онлайн сканеров безопасности

Достаточно ли использовать 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-токены для защиты от межсайтовой подделки запросов
Для углубленного изучения CSRF-атак рекомендую ознакомиться с анализом современных web-уязвимостей и методов обхода защиты, где подробно разобраны техники эксплуатации и защиты.

Что делать при обнаружении SQL инъекции в продакшене?​

План действий:
  1. Немедленно изолировать уязвимый функционал
  2. Проанализировать логи на предмет эксплуатации
  3. Применить временный фикс (WAF правило)
  4. Разработать и протестировать постоянное решение
  5. Провести аудит на похожие уязвимости
  6. Уведомить пользователей если были скомпрометированы данные

Заключение​

Защита от SQL инъекций требует комплексного подхода: от правильного написания кода до настройки инфраструктуры. Используйте prepared statements как основу защиты, добавляйте валидацию данных, применяйте принцип наименьших привилегий и регулярно тестируйте безопасность. Помните, что безопасность это не разовая задача, а постоянный процесс.

Начните с внедрения prepared statements во всех новых проектах, постепенно рефакторьте старый код и обязательно настройте мониторинг подозрительной активности. Современные инструменты и фреймворки делают защиту от SQL инъекций проще, но требуют понимания принципов безопасности для правильного применения.

Полезные ресурсы для углубленного изучения​

  • Практика: root-me.org, alexbers.com/sql/, dvwa.co.uk
Начало здесь: Безопасный PHP. Защита от XSS атак
 

Вложения

  • 5ZyfKYLxSM40DJxnK3SQ_05174717707032-t1200x480.jpg
    5ZyfKYLxSM40DJxnK3SQ_05174717707032-t1200x480.jpg
    34,5 КБ · Просмотры: 1 407
Последнее редактирование модератором:
Как это понять?
Скрытое содержимое, Вам необходимо иметь сообщений: 0, а сейчас у Вас сообщений:2.
 
PHP:
$id = $_GET['id'] ?? 'Пусто';
$connect = new PDO('mysql:dbname=codeby;host=localhost', 'root', '');
$sql = "SELECT username, password FROM codeby_sql WHERE id = :id";
$sth = $connect->prepare($sql, [PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY]);
$sth->execute([':id' => $id]);

while ($row = $sth->fetch()) {
    echo 'Username: ' .$row['username']. '<br>';
    echo 'Password: ' .$row['password'];
}
}
Последняя скобка лишняя

PHP:
$id = $_GET['id'] ?? 'Пусто';
$connect = new mysqli('localhost', 'root', '', 'codeby');
$query = "SELECT username, password FROM codeby_sql WHERE id = ?";
$sth = $mysqli->stmt_init();

if ($sth->prepare($query)) {
    $sth->bind_param("i", $id);
    $sth->execute();

    $result = $sth->get_result();

    while ($row = $result->fetch_array(MYSQLI_NUM)) {
        echo 'Username: ' .$row['username']. '<br>';
        echo 'Password: ' .$row['password'];
    }
}
- Откуда взялась переменная $mysqli?
- Если указываете MYSQLI_NUM, то нужно использовать числовые индексы
PHP:
        echo 'Username: ' .$row[0]. '<br>';
        echo 'Password: ' .$row[1];
 
  • Нравится
Реакции: Vertigo и r0hack
В 21 веке SQL инъекция она не опасная, так как полно методов защиты.
Те же самые подготовленные запросы, или к примеру регулярные выражения.
Методами PHP. и т.д.
 
В 21 веке SQL инъекция она не опасная, так как полно методов защиты.
Те же самые подготовленные запросы, или к примеру регулярные выражения.
Методами PHP. и т.д.
Что за бред ты несёшь? Может ты про слепые скули не слышал?
 
  • Нравится
Реакции: r0hack
Мы в соцсетях:

Взломай свой первый сервер и прокачай скилл — Начни игру на HackerLab

Похожие темы