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, никогда +. Без исключений. Даже если переменная «своя» и «из БД» — особенно если из БД.
Продолжить чтение
Что бы прочитать модуль полностью, зарегистрируйтесь/войдите на платформу
Когда закончишь — отметь раздел, чтобы продолжить.