SQLLab
Все статьи

SQL-инъекции: как работают и как защититься

SQL-инъекции: как работает атака, классические примеры, parameterized queries, prepared statements, принцип наименьших привилегий. Защита на практике.

8 марта 2026 г.·5 мин чтения·

SQL-инъекция — одна из старейших и при этом до сих пор актуальных уязвимостей. По данным OWASP, она стабильно входит в топ-3 угроз для веб-приложений. Понимать, как она работает, важно не только для разработчиков, но и для всех, кто работает с базами данных.

Классический пример: обход аутентификации

Представьте форму входа. Бэкенд формирует SQL-запрос конкатенацией строк:

# ОПАСНЫЙ КОД — никогда так не делайте
username = request.POST['username']
password = request.POST['password']

query = f"""
    SELECT id FROM users
    WHERE username = '{username}'
    AND password = '{password}'
"""

Если пользователь вводит в поле username значение:

admin' --

Итоговый запрос становится:

SELECT id FROM users
WHERE username = 'admin' --' AND password = '...'

Символы -- — комментарий в SQL. Всё после них игнорируется. Условие проверки пароля исчезает, и атакующий входит как admin без знания пароля.

UNION-based инъекция: кража данных

Более опасный вариант — UNION-инъекция, позволяющая извлекать данные из произвольных таблиц:

-- Уязвимый запрос для поиска товаров
-- /search?q=phone
SELECT name, price FROM products WHERE name LIKE '%phone%'

-- Атакующий вводит:
-- /search?q=phone' UNION SELECT username, password FROM users --

-- Итоговый запрос:
SELECT name, price FROM products WHERE name LIKE '%phone%'
UNION
SELECT username, password FROM users --'

В ответе API вместо товаров вернутся логины и пароли всех пользователей. Если пароли хранятся в открытом виде — катастрофа.

Защита: параметризованные запросы

Единственно надёжный способ защиты — параметризованные запросы (parameterized queries / prepared statements). Данные пользователя передаются отдельно от SQL-кода и никогда не интерпретируются как часть запроса.

Python (psycopg2)

import psycopg2

conn = psycopg2.connect("dbname=mydb user=app")
cur = conn.cursor()

# ПРАВИЛЬНО: параметры передаются отдельно
username = request.POST['username']
password = request.POST['password']

cur.execute(
    "SELECT id FROM users WHERE username = %s AND password = %s",
    (username, password)  # psycopg2 экранирует значения
)
user = cur.fetchone()

Даже если username = "admin' --", psycopg2 экранирует кавычку и передаст её как литеральный символ, а не как часть SQL.

Node.js (pg)

const { Pool } = require('pg');
const pool = new Pool();

// ПРАВИЛЬНО: параметризованный запрос
async function getUser(username, password) {
    const result = await pool.query(
        'SELECT id FROM users WHERE username = $1 AND password = $2',
        [username, password]  // $1, $2 — позиционные параметры
    );
    return result.rows[0];
}

// ОПАСНО: конкатенация
async function getUserUnsafe(username, password) {
    const result = await pool.query(
        `SELECT id FROM users WHERE username = '${username}'`
    );
    return result.rows[0];
}

Prepared Statements в PostgreSQL

-- Подготовленный запрос на уровне сессии
PREPARE get_user (text, text) AS
    SELECT id, username FROM users
    WHERE username = $1 AND password_hash = crypt($2, password_hash);

-- Выполнение с параметрами
EXECUTE get_user('admin', 'my_password');

-- Освобождение
DEALLOCATE get_user;

Принцип наименьших привилегий

Параметризация защищает от инъекций, но не отменяет важность правильной настройки прав доступа. Приложение должно работать от пользователя с минимально необходимыми правами:

-- Создаём отдельного пользователя для приложения
CREATE USER app_user WITH PASSWORD 'secure_password';

-- Даём только нужные права, не суперпользователя
GRANT SELECT, INSERT, UPDATE ON orders TO app_user;
GRANT SELECT ON products TO app_user;
GRANT SELECT ON users TO app_user;

-- НЕ даём права на удаление и DDL
-- REVOKE DELETE ON orders FROM app_user;
-- REVOKE ALL ON pg_user FROM app_user; -- и так нет, но явно зафиксируем

-- Проверить текущие права
SELECT grantee, table_name, privilege_type
FROM information_schema.role_table_grants
WHERE grantee = 'app_user'
ORDER BY table_name;

Если атакующий всё же найдёт инъекцию, он сможет делать только то, что разрешено app_user — например, читать заказы, но не удалять таблицы и не читать системные каталоги.

Мониторинг подозрительной активности

-- Найти запросы с подозрительными паттернами в pg_stat_activity
-- (для активных сессий прямо сейчас)
SELECT
    pid,
    usename,
    application_name,
    client_addr,
    now() - query_start AS duration,
    left(query, 200)    AS query
FROM pg_stat_activity
WHERE query ILIKE '%union%select%'
   OR query ILIKE '%--%'
   OR query ILIKE '%information_schema%'
   OR query ILIKE '%pg_tables%';

Что ещё помогает

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

ORM (SQLAlchemy, Django ORM, Hibernate) автоматически используют параметризованные запросы для стандартных операций. Опасность возникает при использовании raw() / execute() с конкатенацией строк.

WAF (Web Application Firewall) может блокировать типичные инъекционные паттерны, но не является заменой параметризации — это дополнительный слой защиты.

Итог

Метод защитыНадёжностьПримечание
Параметризованные запросыВысокаяОсновной метод
Prepared statementsВысокаяАналог, работает на уровне БД
Наименьшие привилегииСредняяОграничивает последствия
ORMВысокаяПри правильном использовании
Экранирование строк вручнуюНизкаяНенадёжно, избегайте

SQL-инъекции предотвращаются на уровне кода, а не конфигурации. Параметризованные запросы — не опция, а обязательное требование для любого production-приложения.


SQL-инъекции — частая тема на технических собеседованиях. Подготовьтесь к ним в разделе Подготовка к собесу или отработайте запросы в тренажёре.

Похожие статьи

Попробуй на практике

Тренажёр с реальными задачами — бесплатно и без регистрации

Открыть тренажёр →