Webrium
Подписаться

SQL-инъекция: как она работает и чем закрывается

Как защитить приложения от SQL-инъекций и почему классические методы не работают

Стык данных и кода в SQL-запросеВвод как данныеВвод как код

Как SQL-инъекция превращает пользовательский ввод в исполняемый код

Представьте ситуацию: вы пишете обычное письмо, а почтовый сервер вдруг решает, что часть текста - это инструкция для себя. Например, строка “Удали все письма из папки Входящие” воспринимается как команда, и сервер безоговорочно выполняет ее. Примерно так работает SQL-инъекция - база данных принимает пользовательские данные за часть SQL-запроса и исполняет их как код, хотя предполагалось, что это просто данные.

Вот как это происходит на практике. Допустим, у нас есть простое веб-приложение с формой входа. Когда пользователь вводит логин, приложение формирует запрос к базе данных:

SELECT * FROM users WHERE login = '$login'

Казалось бы, безобидная конструкция. Но если злоумышленник введет в поле логина строку ' OR '1'='1, запрос превратится в:

SELECT * FROM users WHERE login = '' OR '1'='1'

База данных вернет все записи из таблицы users, потому что условие '1'='1' всегда истинно. Злоумышленник получает доступ ко всем учетным записям без пароля. Но это лишь начало - инъекция может не только читать данные, но и изменять их, удалять таблицы или даже выполнять произвольные команды на сервере базы данных.

Почему традиционные методы защиты неэффективны

Проблемы экранирования символов

Первая реакция на обнаружение уязвимости - экранирование опасных символов. Казалось бы, достаточно заменить одинарные кавычки на две, и проблема решена. Однако этот подход содержит несколько критических недостатков:

  1. Проблема совместимости кодировок: Современные приложения работают с множеством кодировок. Если приложение передает данные в одной кодировке (например, UTF-8), а база данных ожидает другую (например, Windows-1251), экранирование может не сработать. Символы вроде обратного слэша или одинарной кавычки могут кодироваться по-разному в разных кодировках, и база данных не распознает их как экранирующие.

  2. Контекстная зависимость: Экранирование эффективно только для строковых литералов. В числовых полях, именах таблиц, именах колонок или частях SQL-выражений кавычки не нужны. Например, если в числовое поле передать 1; DROP TABLE users, база данных выполнит это как два отдельных запроса: сначала выборку, а затем удаление таблицы.

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

  4. Человеческий фактор: Экранирование легко забыть или применить некорректно, особенно в сложных запросах с множеством параметров.

Несостоятельность чёрных списков

Другой распространенный подход - создание чёрных списков запрещенных слов и конструкций. Однако этот метод страдает от нескольких фундаментальных проблем:

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

  2. Обходные пути: Злоумышленники постоянно находят новые способы обхода чёрных списков. Например:

    • Использование комментариев для разбиения слов: O/**/R, DR/**/OP
    • Альтернативные кодировки символов
    • Использование синонимов и эквивалентных конструкций
  3. Ложные срабатывания: Запрет распространенных слов ломает легитимные данные. Например:

    • Фамилии (O’Reilly, D’Arcy)
    • Текстовые данные, содержащие SQL-подобные конструкции (“selective choice”, “order by date”)
    • Технические термины и коды
  4. Поддержка и сопровождение: Чёрные списки требуют постоянного обновления по мере появления новых способов инъекций и новых версий SQL.

Параметризованные запросы: почему это единственное надежное решение

Параметризованные запросы решают проблему SQL-инъекций радикально и надежно. Вместо склейки строк с пользовательскими данными они передают данные отдельно от кода запроса:

# Небезопасный подход: склейка строк
cursor.execute(f"SELECT * FROM users WHERE login = '{login}'")

# Безопасный подход: параметризованный запрос
cursor.execute("SELECT * FROM users WHERE login = %s", (login,))

Механизм работы параметризованных запросов основан на четком разделении кода и данных:

  1. Явное разделение: База данных получает запрос и данные отдельно. Она знает, что параметры - это данные, а не часть SQL-кода.

  2. Типизация параметров: Каждый параметр имеет строго определенный тип данных (строка, число, дата и т.д.), что предотвращает его интерпретацию как кода.

  3. Универсальность: Подход работает во всех современных СУБД (PostgreSQL, MySQL, SQL Server, Oracle) и языках программирования (Python, Java, C#, PHP, JavaScript).

  4. Автоматическая обработка: Драйвер базы данных автоматически обрабатывает экранирование и форматирование параметров в соответствии с требованиями конкретной СУБД.

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

Ограничения параметризованных запросов и альтернативные решения

Что нельзя параметризовать

Параметризованные запросы защищают только значения в условиях WHERE, VALUES и SET. Однако существуют случаи, когда они не работают:

  1. Имена таблиц: Например, SELECT * FROM $table WHERE id = 1. Если имя таблицы задается пользователем, инъекция неизбежна.

  2. Имена колонок: Например, SELECT $column FROM users WHERE id = 1. Здесь можно подставить имя колонки с последующей инъекцией.

  3. Направление сортировки: Например, SELECT * FROM users ORDER BY id $sort_direction. Злоумышленник может передать вредоносный код вместо направления сортировки.

  4. SQL-выражения: Например, SELECT * FROM users WHERE $condition. Любое выражение, содержащее SQL-код, потенциально уязвимо.

Белые списки как решение

Для этих случаев единственный надежный подход - использование белых списков. Белый список содержит только разрешенные значения, и любое отклонение от них отвергается:

# Пример для сортировки
sort_directions = {'asc': 'ASC', 'desc': 'DESC'}
sort_direction = sort_directions.get(user_input, 'ASC')
query = f"SELECT * FROM users ORDER BY id {sort_direction}"

# Пример для имен таблиц
allowed_tables = {'users', 'products', 'orders'}
if table_name not in allowed_tables:
    raise ValueError("Invalid table name")
query = f"SELECT * FROM {table_name}"

# Пример для имен колонок
allowed_columns = {'id', 'name', 'email', 'created_at'}
if column_name not in allowed_columns:
    raise ValueError("Invalid column name")
query = f"SELECT {column_name} FROM users"

Белые списки требуют больше усилий при реализации, но обеспечивают надежную защиту. Они особенно важны в следующих случаях:

  1. Динамические отчеты: Когда пользователи могут выбирать, какие данные отображать.

  2. Конструкторы запросов: Когда приложение позволяет пользователям формировать собственные запросы.

  3. Административные интерфейсы: Когда администраторы могут выполнять произвольные запросы.

Скрытые угрозы: где инъекция прячется в неочевидных местах

Динамическая сортировка и фильтрация

Даже простые операции сортировки и фильтрации могут стать источником уязвимостей:

-- Уязвимо к инъекции
SELECT * FROM users ORDER BY $column $direction

Злоумышленник может передать id; DROP TABLE users; -- в качестве имени колонки для сортировки. Аналогичные проблемы возникают с динамической фильтрацией:

-- Уязвимо к инъекции
SELECT * FROM users WHERE $filter_condition

Конструкторы запросов и ORM

Многие разработчики ошибочно считают ORM (Object-Relational Mapping) безопасными по умолчанию:

# Опасно в SQLAlchemy
session.query(User).filter(f"login = '{login}'")

# Опасно в Django
User.objects.filter(f"login = '{login}'")

Даже ORM могут склеивать строки под капотом, создавая уязвимости. Безопасные варианты:

# Безопасно в SQLAlchemy
session.query(User).filter(User.login == login)

# Безопасно в Django
User.objects.filter(login=login)

Хранимые процедуры

Хранимые процедуры не являются панацеей от инъекций и сами могут быть уязвимы:

-- Уязвимая процедура в PostgreSQL
CREATE OR REPLACE PROCEDURE get_user(p_login TEXT) AS $$
BEGIN
    EXECUTE 'SELECT * FROM users WHERE login = ''' || p_login || '''';
END;
$$ LANGUAGE plpgsql;

Передача admin' -- приведет к выполнению:

SELECT * FROM users WHERE login = 'admin' --'

Безопасный вариант:

-- Безопасная процедура
CREATE OR REPLACE PROCEDURE get_user(p_login TEXT) AS $$
BEGIN
    EXECUTE 'SELECT * FROM users WHERE login = $1' USING p_login;
END;
$$ LANGUAGE plpgsql;

Поиск по подстроке

Оператор LIKE с пользовательским вводом открывает возможности для инъекций:

SELECT * FROM users WHERE name LIKE '%$search%'

Передача %' OR '1'='1 вернет все записи из таблицы. Символ % в LIKE означает “любая последовательность символов”, и злоумышленник может использовать его для обхода условий.

Конкатенация строк в SQL

Даже при использовании параметров конкатенация строк внутри SQL может привести к инъекциям:

-- Уязвимо
EXECUTE 'SELECT * FROM users WHERE login = ''' || p_login || '''';

Динамический SQL в приложениях

Многие приложения формируют SQL-запросы динамически на основе пользовательского ввода:

# Уязвимо
def build_query(table, condition):
    return f"SELECT * FROM {table} WHERE {condition}"

Права доступа: вторая линия обороны

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

Даже если инъекция произошла, ее последствия можно ограничить, следуя принципу минимальных привилегий:

  1. Разделение ролей: Приложение не должно подключаться к базе данных от имени суперпользователя или администратора.

  2. Ограниченные права: Пользователь базы данных должен иметь доступ только к тем таблицам и операциям, которые необходимы для работы приложения.

  3. Разделение операций чтения и записи: Для операций записи должны быть отдельные права (INSERT, UPDATE), отличные от прав на удаление (DROP, TRUNCATE, DELETE).

Примеры настройки прав в различных СУБД

В PostgreSQL:

-- Создание пользователя с ограниченными правами
CREATE USER app_user WITH PASSWORD 'secure_password_here';

-- Предоставление доступа к базе данных
GRANT CONNECT ON DATABASE app_db TO app_user;

-- Предоставление прав только на необходимые таблицы
GRANT SELECT, INSERT, UPDATE ON TABLE users TO app_user;
GRANT SELECT ON TABLE products TO app_user;

-- Запрет всех остальных операций
REVOKE ALL ON SCHEMA public FROM app_user;

В MySQL:

-- Создание пользователя с ограниченными правами
CREATE USER 'app_user'@'localhost' IDENTIFIED BY 'secure_password_here';

-- Предоставление прав только на необходимые операции
GRANT SELECT, INSERT, UPDATE ON app_db.users TO 'app_user'@'localhost';
GRANT SELECT ON app_db.products TO 'app_user'@'localhost';

-- Запрет всех остальных операций
REVOKE DROP, DELETE, ALTER, CREATE ON app_db.* FROM 'app_user'@'localhost';

В SQL Server:

-- Создание пользователя с ограниченными правами
CREATE LOGIN app_user WITH PASSWORD = 'secure_password_here';
USE app_db;
CREATE USER app_user FOR LOGIN app_user;

-- Предоставление прав только на необходимые таблицы
GRANT SELECT, INSERT, UPDATE ON users TO app_user;
GRANT SELECT ON products TO app_user;

-- Запрет всех остальных операций
DENY DELETE, DROP, ALTER ON users TO app_user;
DENY DELETE, DROP, ALTER ON products TO app_user;

Практическая проверка кода на уязвимости

Поиск опасных конструкций

  1. Склейка строк в любых формах:

    # Опасно во всех языках программирования
    query = "SELECT * FROM users WHERE login = '" + login + "'"
    query = f"SELECT * FROM users WHERE login = '{login}'"
    query = "SELECT * FROM users WHERE login = '{}'".format(login)
  2. Сырые запросы в ORM:

    # Опасно в Django ORM
    User.objects.raw(f"SELECT * FROM users WHERE login = '{login}'")
    User.objects.extra(where=[f"login = '{login}'"])
    
    # Опасно в SQLAlchemy
    session.query(User).filter(f"login = '{login}'")
    session.execute(f"SELECT * FROM users WHERE login = '{login}'")
  3. Динамический SQL в хранимых процедурах:

    -- Опасно в PostgreSQL
    EXECUTE 'SELECT * FROM users WHERE login = ''' || p_login || '''';
    
    -- Опасно в SQL Server
    EXEC('SELECT * FROM users WHERE login = ''' + @login + '''');
  4. Динамические части запросов:

    # Опасно: динамические имена таблиц и колонок
    query = f"SELECT * FROM {table_name} WHERE {column_name} = '{value}'"
    
    # Опасно: динамические выражения сортировки
    query = f"SELECT * FROM users ORDER BY {sort_column} {sort_direction}"

Настройка логирования для выявления уязвимостей

В PostgreSQL:

-- Включение логирования всех запросов
ALTER SYSTEM SET log_statement = 'all';

-- Логирование времени выполнения запросов
ALTER SYSTEM SET log_duration = on;

-- Логирование ошибок
ALTER SYSTEM SET log_min_error_statement = 'error';

-- Применение изменений
SELECT pg_reload_conf();

В MySQL:

-- Включение общего лога (осторожно, может сильно нагружать сервер)
SET GLOBAL general_log = 'ON';
SET GLOBAL general_log_file = '/var/log/mysql/mysql-general.log';

-- Альтернатива: логирование медленных запросов
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 0; -- Логировать все запросы
SET GLOBAL slow_query_log_file = '/var/log/mysql/mysql-slow.log';

В SQL Server:

-- Включение логирования всех запросов
USE master;
EXEC sp_configure 'show advanced options', 1;
RECONFIGURE;
EXEC sp_configure 'default trace enabled', 1;
RECONFIGURE;

-- Настройка Extended Events для логирования запросов
CREATE EVENT SESSION [QueryLogging] ON SERVER
ADD EVENT sqlserver.sql_statement_completed
ADD TARGET package0.event_file(SET filename=N'QueryLogging')
WITH (MAX_MEMORY=4096 KB, MAX_DISPATCH_LATENCY=30 SECONDS);
ALTER EVENT SESSION [QueryLogging] ON SERVER STATE = START;

Анализ логов

При анализе логов следует обращать внимание на:

  1. Запросы с пользовательскими данными внутри SQL-кода: Любые запросы, содержащие конкатенацию строк с пользовательским вводом.

  2. Динамические SQL-запросы: Запросы, формируемые на лету с использованием пользовательского ввода.

  3. Необычные шаблоны запросов: Повторяющиеся запросы с разными значениями параметров могут указывать на попытки инъекций.

  4. Ошибки базы данных: Частые ошибки синтаксиса SQL могут свидетельствовать о попытках инъекций.

Почему фильтры перед приложением - тупиковый путь

Ограничения WAF и аналогичных решений

Многие разработчики и администраторы считают, что установка WAF (Web Application Firewall) или аналогичных фильтров перед приложением решит проблему SQL-инъекций. Однако этот подход имеет несколько фундаментальных ограничений:

  1. Отсутствие контекста: WAF не понимает контекст приложения и не может отличить легитимные данные от вредоносных. Например:

    • Фамилия O’Reilly содержит одинарную кавычку, которая может быть ошибочно заблокирована
    • Технические термины, содержащие SQL-подобные конструкции (“order by date”)
    • Легитимные данные, содержащие специальные символы
  2. Возможность обхода: Злоумышленники постоянно находят новые способы обхода WAF:

    • Альтернативные кодировки символов (URL-кодирование, Unicode)
    • Использование комментариев для разбиения слов (O/**/R, DR/**/OP)
    • Разбиение запросов на части с использованием различных техник
    • Использование нестандартных пробелов и символов
  3. Ложные срабатывания: WAF может блокировать легитимные запросы, что приводит к проблемам с функциональностью приложения.

  4. Производительность: Анализ каждого запроса на уровне WAF может существенно снижать производительность приложения.

  5. Сложность настройки: Правильная настройка WAF требует глубоких знаний как о приложении, так и о возможных векторах атак.

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

Альтернативы WAF

Вместо того чтобы полагаться на WAF, лучше использовать комплексный подход:

  1. Параметризованные запросы как основной метод защиты
  2. Белые списки для динамических частей запросов
  3. Минимальные права доступа для пользователей базы данных
  4. Логирование и мониторинг всех запросов к базе данных
  5. Регулярные проверки кода на наличие уязвимостей

Позиция редакции

SQL-инъекция - это не технический баг конкретной СУБД или языка программирования, а фундаментальная ошибка проектирования приложений. База данных исполняет то, что ей передают, не различая код и данные. Поэтому защита от SQL-инъекций должна быть встроена в архитектуру приложения на всех уровнях:

  1. Параметризованные запросы как основной и обязательный метод защиты от инъекций. Они должны использоваться везде, где это возможно.

  2. Белые списки для всех динамических частей запросов (имена таблиц, колонок, направления сортировки и т.д.). Это единственный надежный способ обработки таких данных.

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

  4. Логирование всех запросов к базе данных. Это позволяет быстро обнаруживать уязвимости и отслеживать попытки атак.

  5. Регулярные проверки кода на наличие потенциальных уязвимостей. Автоматизированные инструменты и ручные проверки должны быть частью процесса разработки.

Эти меры универсальны и не зависят от используемых технологий - они работают в любых языках программирования и СУБД. Однако они требуют глубокого понимания механизмов SQL-инъекций и дисциплины в разработке.

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

  • GraphQL с жестко заданными схемами
  • ORM, которые не позволяют исполнять сырые SQL-запросы
  • Специализированные DSL (Domain-Specific Languages) для работы с данными

Однако даже в этих случаях необходимо тщательно проверять реализацию на наличие потенциальных уязвимостей. Например, если ORM позволяет передавать сырые SQL-выражения в фильтры, это потенциальная уязвимость. Абсолютная безопасность возможна только при полном исключении SQL из приложения, что на практике встречается крайне редко.

С чем это связано

Метка над заголовком говорит, зачем туда идти: продолжить тему, перейти к практике или увидеть возражение.

Схема разделенного поля: слева заголовки безопасности добавлены, справа сайт не работает из-за неправильной настройкиЗаголовки добавленыСайт сломался
Практика

Заголовки безопасности: какие ставить и что они ломают

Защитите приложение от атак через заголовки: какие настройки работают, а какие только создают иллюзию безопасности.

Время съедает один шаг плана выполнения, а не запрос целиком: искать надо его, а не вешать индекс наугадшаги планадорогой шаг
Предыстория

Медленный запрос в PostgreSQL: с чего начинать

Как диагностировать медленный запрос в PostgreSQL без лишних индексов — с проверенной последовательностью шагов.

Три показателя безопасности паролей: два в норме, один за шкалой — отсутствие солиБез соли
Усиление

Пароли пользователей: как хранить и что проверить сегодня

Практические шаги по защите паролей — от проверки алгоритмов хеширования до поиска уязвимостей в коде.

Письмо по вторникам

Один разбор недели и короткий список того, что изменилось. Без дайджестов на сорок ссылок.

Начните вводить — материалы появятся здесь.

выбрать · Enter открыть · Esc закрыть