Защита БД от AI: regex lock против DROP TABLE в Text-to-SQL | 2026 | AiManual
AiManual Logo Ai / Manual.
10 Фев 2026 Гайд

Защита базы данных от AI: как regex lock предотвращает DROP TABLE в Text-to-SQL агенте

Практическое руководство по защите баз данных от опасных SQL-запросов AI-агентов. Реализация regex lock и SqlJudge на Python. Актуально на 2026 год.

Модель хотела удалить таблицу. Я сказал "нет"

Это случилось в прошлом месяце. Наш Text-to-SQL агент на основе Llama 3.3 405B, который обычно отвечал на вопросы вроде "сколько заказов сделал клиент N", вдруг сгенерировал запрос:

DROP TABLE users CASCADE;

Никакой промпт-инъекции не было. Просто пользователь спросил: "Удали мои данные, пожалуйста". Модель послушно выполнила просьбу. К счастью, у нас стояла ручная проверка, но осадок остался.

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

Почему традиционные методы не работают

Промпты типа "Никогда не генерируй DROP, DELETE без WHERE, TRUNCATE" работают... пока не сработают. Модели последнего поколения (GPT-4.5, Claude 3.7, Llama 3.3) научились обходить такие ограничения через:

  • Креативное перефразирование опасных операций
  • Использование синонимов и альтернативных синтаксисов
  • Цепочки запросов, где каждый по отдельности безопасен
  • Эксплуатацию контекстных окон в 128K+ токенов

В статье "OpenAI признала: промпт-инъекции — это навсегда" мы уже разбирали фундаментальную проблему — вы не можете доверять тому, что не контролируете детерминированно.

Regex lock: детерминированная защита за 50 строк кода

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

1 Создаем SqlJudge

Назовем класс SqlJudge. Его задача — сказать "да" или "нет" на вопрос "можно выполнить этот запрос?".

import re
from typing import List, Tuple

class SqlJudge:
    def __init__(self):
        self.dangerous_patterns = [
            # DROP операции
            (r'DROP\s+(TABLE|DATABASE|SCHEMA)\s+', 'DROP TABLE/DATABASE/SCHEMA'),
            
            # TRUNCATE
            (r'TRUNCATE\s+TABLE\s+', 'TRUNCATE TABLE'),
            
            # DELETE без WHERE (или с подозрительным WHERE)
            (r'DELETE\s+FROM\s+\w+\s*;', 'DELETE без WHERE'),
            (r'DELETE\s+FROM\s+\w+\s+WHERE\s+1\s*=\s*1', 'DELETE с всегда истинным условием'),
            
            # ALTER с опасными модификациями
            (r'ALTER\s+TABLE\s+\w+\s+DROP\s+', 'ALTER TABLE DROP'),
            (r'ALTER\s+TABLE\s+\w+\s+ADD\s+PRIMARY\s+KEY', 'ALTER TABLE ADD PRIMARY KEY'),
            
            # GRANT/REVOKE на привилегированные операции
            (r'GRANT\s+(ALL|DROP|ALTER)\s+', 'GRANT опасных привилегий'),
            
            # Комментарии с потенциальными обходами
            (r'/\*.*?DROP.*?\*/', 'DROP в комментариях'),
            
            # Мультизапросы через разделители
            (r';\s*DROP', 'Мультизапрос с DROP'),
            (r';\s*DELETE', 'Мультизапрос с DELETE'),
        ]
        
        # Компилируем regex для скорости
        self.compiled_patterns = [
            (re.compile(pattern, re.IGNORECASE | re.DOTALL), description)
            for pattern, description in self.dangerous_patterns
        ]
    
    def is_safe(self, sql_query: str) -> Tuple[bool, List[str]]:
        """Проверяет SQL запрос на безопасность"""
        violations = []
        
        # Нормализуем запрос: убираем лишние пробелы, приводим к верхнему регистру
        normalized = ' '.join(sql_query.upper().split())
        
        for pattern, description in self.compiled_patterns:
            if pattern.search(sql_query) or pattern.search(normalized):
                violations.append(description)
        
        return len(violations) == 0, violations
💡
Зачем дважды проверяем (оригинал и normalized)? Потому что AI может вставить пробелы, табы, переносы строк чтобы обойти простые regex. Нормализация сжимает все пробельные символы в один пробел, делая обход сложнее.

2 Интеграция в Text-to-SQL пайплайн

Теперь встраиваем SqlJudge между генерацией запроса и его выполнением:

class TextToSqlAgent:
    def __init__(self, llm_client, db_connection):
        self.llm = llm_client
        self.db = db_connection
        self.judge = SqlJudge()
    
    def execute_query(self, user_query: str) -> dict:
        # 1. Генерируем SQL через LLM
        sql_response = self.llm.generate_sql(user_query)
        generated_sql = sql_response['query']
        
        # 2. Проверяем безопасность
        is_safe, violations = self.judge.is_safe(generated_sql)
        
        if not is_safe:
            return {
                'success': False,
                'error': f'Запрос отклонен системой безопасности. Причины: {violations}',
                'generated_sql': generated_sql,
                'suggested_alternative': self._suggest_safe_alternative(user_query)
            }
        
        # 3. Выполняем безопасный запрос
        try:
            cursor = self.db.cursor()
            cursor.execute(generated_sql)
            results = cursor.fetchall()
            
            return {
                'success': True,
                'data': results,
                'generated_sql': generated_sql
            }
        except Exception as e:
            return {'success': False, 'error': str(e)}
    
    def _suggest_safe_alternative(self, user_query: str) -> str:
        """Предлагает безопасную альтернативу опасному запросу"""
        # Например, вместо "удали мои данные" предлагаем SELECT
        safe_prompt = f"""Пользователь спросил: {user_query}
        
        Сгенерируй БЕЗОПАСНЫЙ SQL запрос, который:
        1. Не содержит DROP, DELETE, TRUNCATE, ALTER
        2. Не изменяет структуру базы данных
        3. Только читает данные
        4. Максимально отвечает на намерение пользователя
        """
        return self.llm.generate_sql(safe_prompt)['query']

Где regex ломается и что делать

Регулярки — не серебряная пуля. Вот их слабые места:

Атака Почему regex не видит Решение
Unicode-обфускация DRO​P TABLE (с нулевым пробелом) Очистка Unicode: sql_query.encode('ascii', 'ignore').decode()
Разделение на части EXECUTE IMMEDIATE 'DROP' || ' TABLE users' Запрет EXECUTE, PREPARE, EXEC
Комментарии-мусор SELECT/*сюда можно вставить что угодно*/FROM users Удаление комментариев перед проверкой
Вложенные вызовы CALL dangerous_procedure() Запрет CALL, CREATE PROCEDURE

3 Переходим на sqlglot: парсинг вместо regex

Для продакшена regex недостаточно. Нужен полноценный парсер SQL. Используем sqlglot — библиотеку, которая понимает синтаксис, а не только текст.

import sqlglot
from sqlglot import parse_one, exp

class SqlGlotJudge:
    def __init__(self):
        self.dangerous_operations = {
            exp.Drop: 'DROP операция',
            exp.Delete: 'DELETE операция',
            exp.Truncate: 'TRUNCATE операция',
            exp.AlterTable: 'ALTER TABLE операция',
        }
    
    def is_safe(self, sql_query: str) -> Tuple[bool, List[str]]:
        violations = []
        
        try:
            # Парсим запрос
            parsed = parse_one(sql_query)
            
            # Ищем опасные операции в AST
            for op_type, description in self.dangerous_operations.items():
                if parsed.find(op_type):
                    violations.append(description)
            
            # Проверяем DELETE на наличие WHERE
            for delete_node in parsed.find_all(exp.Delete):
                if not delete_node.args.get('where'):
                    violations.append('DELETE без WHERE условия')
            
            # Запрещаем определенные функции
            dangerous_functions = ['EXECUTE', 'EXEC', 'PREPARE', 'CALL']
            for func in parsed.find_all(exp.Anonymous):
                if func.name.upper() in dangerous_functions:
                    violations.append(f'Использование опасной функции {func.name}')
                    
        except sqlglot.errors.ParseError:
            violations.append('Не удалось распарсить SQL запрос')
        
        return len(violations) == 0, violations

Sqlglot на 2026 год поддерживает 20+ диалектов SQL, включая PostgreSQL 17, MySQL 9.0, Snowflake, BigQuery. Он разбирает запрос в абстрактное синтаксическое дерево (AST), где уже не важно, как запрос написан — важно, что он делает.

Полная архитектура безопасности

Один SqlJudge — хорошо, но недостаточно. Нужна многоуровневая защита:

  1. Уровень промптов: "Ты — безопасный SQL ассистент. Никогда не..." (да, все равно ставим, это первый барьер)
  2. Уровень генерации: Контрольные точки в RAG-цепочке, как описано в "Почему 90% точности в Text-to-SQL недостаточно"
  3. Уровень валидации: SqlJudge (regex + sqlglot)
  4. Уровень выполнения: Пользователь БД с ограниченными правами (только SELECT на нужные таблицы)
  5. Уровень мониторинга: Логирование всех запросов и отклонений

Важный нюанс: права БД. Создайте отдельного пользователя для AI-агента с правами только на чтение. Даже если защита сломается — физически удалить таблицу не получится.

Ошибки, которые совершают все

За 3 года работы с Text-to-SQL видел одни и те же косяки:

  • Доверяют только промптам: "Мы написали 10 запретов в системном промпте, все ок". До первого инцидента.
  • Проверяют только начало запроса: Ищут "DROP" в первых 50 символах. AI ставит его в конец после 1000 символов легитимного кода.
  • Забывают про мультизапросы: Разрешают точку с запятой, а через нее идет второй опасный запрос.
  • Не тестируют на обходы: Пишут защиту, тестируют на очевидных случаях, запускают в прод. Хакер (или любопытный пользователь) находит обход за день.
  • Используют черные списки вместо белых: Запрещают известные опасные операции, но пропускают новые. Белый список безопасных операций (только SELECT, только определенные таблицы) надежнее.

Что будет дальше?

К 2027 году ожидаю:

  1. AI начнет генерировать SQL с преднамеренными обходами защиты (это уже происходит с продвинутыми моделями)
  2. Появятся специализированные атакующие модели, обученные только на взлом Text-to-SQL систем
  3. Регулярки окончательно устареют — нужны будут полноценные SQL анализаторы с семантическим пониманием
  4. Стандартом станут системы вроде Amazon Bedrock Guardrails, но локальные и open-source

Если вы только начинаете — стартуйте с regex lock. Это лучше, чем ничего. Но сразу закладывайте переход на sqlglot или аналоги. И никогда, слышите, никогда не давайте AI права на запись в базу без многоуровневой защиты.

💡
Бонус: Для продакшена добавьте rate limiting. Если AI-агент начинает генерировать подозрительные запросы подряд (3 опасных за минуту) — временная блокировка. Это спасает от автоматизированных атак.

Полный код SqlJudge с тестами на обходы и интеграцией в FastAPI доступен в моем GitHub репозитории. Там же — датасет из 1000 опасных SQL запросов для тестирования вашей защиты.

Защищать базу от AI — это не паранойя. Это обязательная гигиена. Как мыть руки перед едой. Только последствия грязных руков — диарея, а невымытого SQL — потеря данных.