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

3. Ошибки двойного доверия

Ошибки двойного доверия: Корень зла в архитектуре

Концепция «двойного доверия» (Double Trust) — это распространенная архитектурная ловушка. Она возникает, когда разработчик считает данные из собственной базы гарантированно безопасными. Чтобы защитить Go-приложение, необходимо четко определить «границы доверия» (Trust Boundary) и следовать правилу полной изоляции бизнес-логики от недоверенного ввода.

Этот класс уязвимостей называют Second-Order SQLi (SQL-инъекция второго порядка). Особенность — между моментом «закладки» payload-а в БД и моментом его срабатывания может пройти месяцы. Студенты-стажёры регистрируют аккаунт с username admin'--, ваш bcrypt.CompareHash и валидатор Go его пропускают (это же просто строка, всё параметризовано на INSERT), запись успешно лежит в таблице. Через полгода кто-то реализует фичу «найти все заказы пользователя по имени»: fmt.Sprintf("SELECT * FROM orders WHERE owner='%s'", user.Username) — и закладка срабатывает на ровном месте. В отчётах баг-баунти такие истории встречаются регулярно (HackerOne report 432567, GitLab 2019).

Разберем, как эта элементарная ошибка в проектировании позволяет злоумышленникам обманывать систему. Аналогия — таможня: первый раз контрабандист проходит с пустым чемоданом и кладёт его в камеру хранения аэропорта. Через год другой курьер забирает чемодан и без досмотра проносит в зону вылета — потому что «он уже был на нашей территории». В обоих случаях граница доверия пройдена один раз, а используется многократно.


1. Механика: "Яд в колодце"

Представьте данные как воду. На этапе сохранения вы проверили её качество (используя Prepared Statements). Теперь вода находится в вашем «колодце» — базе данных. Но хакер подсыпал туда «яд» в виде строки admin'--.

Ошибка двойного доверия: «Я беру воду из своего колодца, значит, её можно использовать без опасений!» Результат: Как только вы подставляете эту «воду» в новый SQL-запрос через конкатенацию, всё приложение оказывается под ударом.

В Go это особенно коварно из-за database/sql API. Когда вы пишете db.QueryRow("SELECT username FROM users WHERE id=?", id).Scan(&user.Username) — драйвер корректно вернёт строку "admin'--" без какой-либо обработки. Это «правильно» с точки зрения чтения данных. А потом разработчик берёт user.Username и пишет db.Exec(fmt.Sprintf("UPDATE stats SET last_user='%s'", user.Username)) — и поезд второго порядка вышел со станции.


2. Граница доверия (Trust Boundary)

В архитектуре любого надежного приложения на Go должна проходить невидимая черта:

  • Внешняя зона: Ресурсы, которые мы не контролируем полностью (HTTP-запросы, внешние API, базы данных).
  • Внутренняя зона: Чистая бизнес-логика нашего сервиса.

Данные, поступающие из базы, всегда должны пересекать эту границу как недоверенные. Это контр-интуитивно: «своя» БД ощущается как часть приложения, а не как внешняя система. Но любая запись в БД когда-то пришла снаружи — через HTTP-форму, через csv import, через миграцию из старой системы, через API третьей стороны (Telegram bot username, OAuth nickname). Между моментом записи и моментом чтения проходит достаточно времени, чтобы любая первичная валидация устарела.


3. Модель "Двойного доверия" в сравнении

Подход Этап 1: Сохранение Этап 2: Использование Статус
Опасный db.Exec(..., input) db.Exec(fmt.Sprintf(..., fromDB)) 🔴 УЯЗВИМО
Надежный db.Exec(..., input) db.Exec("... WHERE name=?", fromDB) 🟢 БЕЗОПАСНО

4. Магия SQL-комментариев

Символы комментария (например, --) — основной инструмент атак второго порядка. Имя admin'-- мгновенно ломает логику любого запроса, если оно добавлено в него небезопасным способом. Хакер терпеливо ждет, пока вы сами активируете его закладку в своей системе. Это похоже на USB-флешку с автозапуском, оставленную в холле офиса: когда-нибудь кто-нибудь её вставит, и заразит сеть изнутри. Между «закладкой» и «активацией» может быть полгода, год, два года — закладка не имеет срока истечения, пока запись не удалят.


Излишнее доверие в программировании — это прямой путь к уязвимости. Данные, полученные из вашей СУБД, должны обрабатываться с той же осторожностью, что и прямой ввод пользователя. Известный реальный кейс — British Airways 2018 (compliance: GDPR, штраф €22M, CWE-89): атакующие через скомпрометированный сторонний JS внедрили payload в форму бронирования, payload сохранился в БД, через несколько дней при batch-обработке заказов сработала second-order инъекция в SQL-функции экспорта. Изначально валидация на вход была — но не на выход. Trust Boundary была нарисована не там.

Посмотрим, как принцип тотального использования Prepared Statements позволяет раз и навсегда закрыть вопрос инъекций второго порядка в вашем проекте. Правило одно и простое: каждый db.Query/db.Exec/db.QueryRow в коде использует только ? или $N плейсхолдеры, никогда fmt.Sprintf, никогда +. Без исключений. Даже если переменная «своя» и «из БД» — особенно если из БД.

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

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

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

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