Перейти к содержимому
Назад к пути
Теория 4 мин чтения

6. Защита: почему Prepared Statements работают

Механика защиты: Как работают Prepared Statements под капотом

Чтобы по-настоящему доверять системе защиты, нужно понимать, как она устроена. Когда мы используем подготовленные выражения, общение между Go и базой данных переходит на совершенно другой уровень — протокольный. Это не «магия из учебника», а вполне конкретная последовательность пакетов, которые отправляются через TCP-соединение между Go-приложением и сервером PostgreSQL или MySQL.

Истоки концепции уходят в эпоху ODBC и embedded SQL в конце 1980-х, когда поставщики СУБД (Oracle, Sybase, IBM) обнаружили: повторная компиляция одного и того же запроса с разными значениями параметров — это дорогая операция, и было бы выгодно скомпилировать шаблон один раз и переиспользовать. Так появились Prepared Statements как механизм оптимизации производительности. Защита от SQL-инъекций стала приятным побочным эффектом: раз шаблон уже скомпилирован в plan tree, никакие данные параметров не могут изменить структуру плана. То, что начиналось как performance-фича, превратилось в главный защитный механизм против самого распространённого класса веб-уязвимостей.

Разберем двухэтапный процесс выполнения запроса и поймем, почему в этой схеме кавычки хакера становятся просто безобидным текстом.


1. Механика: PREPARE → BIND → EXECUTE

Когда вы пишете в Go db.Query("SELECT ... WHERE user = ?", u), под капотом происходит три сетевых пакета к базе данных. Это не абстракция — каждый из этих пакетов имеет свой формат, описанный в Wire Protocol Specification соответствующей СУБД (PostgreSQL Extended Query Protocol, MySQL COM_STMT_PREPARE/EXECUTE). Можно открыть Wireshark и буквально посмотреть, как байты летят туда-сюда.

  1. PREPARE: Go отправляет в базу ТОЛЬКО шаблон: SELECT * FROM users WHERE user = ?. База получает этот шаблон, РАЗБИРАЕТ его и сохраняет план выполнения. Она уже ЗНАЕТ, что ? — это данные в строковом поле user.
  2. BIND: Go отправляет в базу данные. Например, имя admin' OR '1'='1. База получает эти данные и просто "кладет" их в то место, где был знак ?.
  3. EXECUTE: База выполняет уже готовый план.

Аналогия: представь, что вы заполняете бумажный бланк банковского перевода. На этапе PREPARE кассир выдаёт вам пустую форму со строго определёнными полями: «Получатель», «Сумма», «Назначение». На этапе BIND вы вписываете в эти поля свои данные — кассир принимает заполненный бланк. На этапе EXECUTE он обрабатывает перевод. Если в поле «Назначение» вы напишете «...а также переведи весь баланс на счёт 12345», кассир не воспримет это как новую инструкцию — он просто запишет эту фразу как назначение платежа. Структура операции уже определена бланком, и никакие данные в полях не могут её изменить. Это и есть Prepared Statement в физическом мире.


2. Ключевая роль: Этап парсинга (Parsing stage)

Почему это защищает от инъекции? Потому что SQL-парсер (программа в базе, которая ищет ключевые слова SELECT, WHERE, OR) срабатывает ТОЛЬКО на этапе PREPARE. На этом этапе хакерских данных там еще нет!

К моменту этапа BIND (когда прилетает ' OR '1'='1), парсер уже закончил свою работу. База больше не ищет в этой строке ключевое слово OR. Она воспринимает его как одну очень длинную и глупую фамилию пользователя.

Это принципиальное архитектурное разделение: control plane (структура запроса) и data plane (значения параметров) передаются разными типами сообщений. В сетевых протоколах эта идея встречается повсеместно — HTTP/2 разделяет HEADERS frame и DATA frame, gRPC отделяет метаданные от полезной нагрузки, TLS использует разные типы записей для handshake и application data. Везде, где важна защита от инъекций (smuggling, поляризация), используется один и тот же принцип: разделяй управляющие данные и пользовательские данные на уровне протокола.


3. Таблица работы: Sprintf vs Prepared

Параметр Использование Sprintf Использование Prepared
Процесс Парсим всю строку целиком Парсим шаблон, BINDим данные
Безопасность 🔴 УЯЗВИМО 🟢 АБСОЛЮТНО БЕЗОПАСНО
Результат Взлом через OR 1=1 Поиск юзера с именем OR 1=1

4. Как это выглядит в PostgreSQL (Wire Protocol)

Если вы заглянете в сетевой трафик (Wireshark), вы увидите, что пэйлоад хакера летит в отдельном пакете данных, а не внутри SQL-команды. Это физическое разделение — и есть ваша главная броня.

В PostgreSQL Extended Query Protocol сообщения имеют типы 'P' (Parse — содержит шаблон запроса), 'B' (Bind — содержит значения параметров), 'E' (Execute — запуск выполнения). Каждое сообщение начинается с однобайтового кода типа и длины, поэтому невозможно физически «перепутать» Parse-сообщение с Bind-сообщением — сервер парсит их по разным веткам кода. Атакующий, который контролирует значение параметра, никогда не сможет «дотянуться» до Parse-сообщения, потому что оно уже улетело в сеть до того, как BIND-данные сформировались.

Драйверы Go, такие как lib/pq и pgx, по умолчанию используют именно Extended Query Protocol для всех запросов с параметрами. То есть, когда вы пишете db.Query("SELECT ... WHERE id = $1", id), под капотом всегда происходит безопасный PREPARE-BIND-EXECUTE цикл — независимо от того, выполняете ли вы запрос один раз или тысячу раз в цикле.


На уровне протокола данные больше не могут повлиять на структуру запроса. Это делает защиту фундаментальной и надежной.

Бонус для производительности: prepared statements можно переиспользовать. Если вы выполняете один и тот же запрос много раз с разными параметрами (например, в цикле обработки batch операций), создайте stmt, _ := db.Prepare(...) один раз и потом вызывайте stmt.Query(args...) многократно. База кэширует план выполнения и пропускает этап PREPARE для последующих вызовов — это даёт прирост скорости 2-5x на heavy-traffic эндпойнтах. В database/sql это работает «из коробки» без дополнительного кода.

Однако безопасности никогда не бывает слишком много. Рассмотрим дополнительные меры защиты, которые помогут укрепить ваше приложение.

Продолжить чтение

Что бы прочитать модуль полностью, зарегистрируйтесь/войдите на платформу

Когда закончишь — отметь раздел, чтобы продолжить.

🚧 Сайт в разработке. Полный функционал пока недоступен. Все вопросы — support@hackandfix.ru