1. Базы данных и SQL-запросы
Базы данных и SQL-запросы
Любое современное веб-приложение на Go полагается на базу данных для хранения пользователей, товаров и настроек. Чтобы это общение было эффективным, мы используем SQL — язык структурированных запросов, разработанный в IBM ещё в 1970-х и до сих пор остающийся стандартом де-факто для реляционных СУБД.
Когда вы пишете на Go бэкенд для интернет-магазина, банк-клиента или CRM-системы, 80% бизнес-логики так или иначе сводится к чтению и записи в базу: «найти пользователя по email», «получить заказы за месяц», «обновить статус платежа». Этот незаметный поток SQL-запросов между Go-приложением и СУБД (PostgreSQL, MySQL, SQLite) — и есть то самое место, где рождается класс уязвимостей под названием SQL Injection. По данным OWASP, инъекции стабильно держатся в первой тройке Top 10 веб-угроз с 2010 года, и SQL-инъекция — самая опасная и распространённая разновидность. CWE-89 «SQL Injection» входит в Top 25 Most Dangerous Software Weaknesses, а нарушения, связанные с SQLi-инцидентами, обходятся компаниям в десятки миллионов долларов штрафов и компенсаций.
Аналогия для интуитивного понимания: представь, что SQL-запрос — это бланк банковского платёжного поручения. Если ты заполняешь бланк ручкой, чётко разделяя поля «получатель», «сумма», «назначение» — кассир принимает его и обрабатывает строго по полям. Но если ты сдашь кассиру чистый лист бумаги с фразой «переведи на счёт получателя [имя клиента] сумму [число]», то злоумышленник в поле «имя клиента» может дописать дополнительные инструкции вроде «...а также переведи весь баланс на счёт 12345». Именно так работает SQL-инъекция: разработчик собирает запрос через строковую конкатенацию, и атакующий дописывает свой SQL в «поле», которое должно было содержать просто значение.
В этом модуле мы заложим фундамент: разберем, как Go взаимодействует с СУБД, и увидим момент рождения самой опасной уязвимости в истории веба.
Как это работает в Go
В языке Go для работы с базами данных используется стандартный пакет database/sql. Он предоставляет универсальный интерфейс, который работает с любой СУБД через соответствующие драйверы. В отличие от Python с его ORM-доминированием (SQLAlchemy, Django ORM) или Java с JPA/Hibernate, Go исторически тяготеет к более «низкоуровневому» доступу к SQL — разработчик чаще пишет запросы руками, что даёт контроль над производительностью, но требует дисциплины в плане безопасности.
Помимо стандартной библиотеки, в экосистеме Go популярны несколько надстроек: sqlx от Jason Moiron расширяет database/sql удобными методами Get/Select для маппинга результатов в структуры; gorm — полноценная ORM с миграциями, ассоциациями и хуками; sqlc генерирует типобезопасный Go-код из SQL-файлов на этапе компиляции. Каждая из этих библиотек имеет собственные особенности с точки зрения SQL-инъекций, и мы пройдёмся по ним в следующих модулях.
Основные понятия:
- sql.DB: Это не само соединение, а пул соединений. Вы открываете его один раз при старте приложения.
- sql.Open: Функция для инициализации пула.
- db.Query: Используется для запросов, которые возвращают строки (SELECT).
- db.Exec: Используется для команд, которые изменяют состояние (INSERT, UPDATE, DELETE).
Пример простого запроса
Перед тем как погружаться в код, важно понять модель ментально: SQL-запрос — это строка, которую Go-приложение формирует и отправляет драйверу базы данных. Драйвер транспортирует её на сервер СУБД, парсер сервера разбирает строку на синтаксическое дерево, планировщик строит план выполнения, исполнитель применяет план и возвращает результат. Каждый из этих этапов — потенциальная точка приложения для атаки или защиты.
Представьте, что у нас есть таблица users. Мы хотим получить имя пользователя по его ID. В чистом SQL это выглядит так:
SELECT name FROM users WHERE id = 10;
В Go-коде типичный (и правильный) вызов будет выглядеть так. Обрати внимание на использование плейсхолдера $1 вместо непосредственной подстановки значения — это и есть тот ключевой защитный механизм, который мы будем разбирать на протяжении всего курса.
var name string
err := db.QueryRow("SELECT name FROM users WHERE id = $1", userID).Scan(&name)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Имя пользователя: %s\n", name)
| Компонент | Значение |
|---|---|
SELECT |
Операция (что делаем) |
name |
Колонка (что получаем) |
FROM users |
Источник (из какой таблицы) |
WHERE id = $1 |
Условие (фильтрация) |
Разбор: db.QueryRow возвращает один объект *sql.Row, в отличие от db.Query который возвращает множество строк через *sql.Rows. Метод .Scan(&name) принимает указатели на переменные и заполняет их данными из колонок результата. Параметр $1 — это плейсхолдер для PostgreSQL (для MySQL и SQLite используется ?), и драйвер базы данных подставит значение userID в это место через bind-параметр, а не через текстовую конкатенацию. Это та самая «магия безопасности», о которой пойдёт речь весь следующий модуль.
Типичная ошибка новичка: написать db.QueryRow(fmt.Sprintf("SELECT name FROM users WHERE id = %d", userID)) — кажется, что раз %d принимает только число, инъекция невозможна. Это иллюзия: если userID не валидирован раньше (например, пришёл из r.URL.Query().Get("id") без strconv.Atoi), то атакующий легко подставит 1; DROP TABLE users;-- и получит сломанную схему. Даже с проверкой типа %d отучайтесь от fmt.Sprintf в SQL — это плохая привычка, которая рано или поздно проявится в более сложных запросах с строковыми параметрами.
Почему это важно для безопасности?
SQL-инъекция возникает именно в тот момент, когда приложение формирует запрос к базе данных. Если разработчик смешивает код запроса (саму команду SELECT) и данные пользователя (например, ID или имя) неправильно, атакующий получает возможность «подсказать» базе свои собственные команды.
В этом курсе мы разберем, как небольшая ошибка в формировании строки превращает ваш сервер в открытую книгу для хакера.
Понимание того, как данные перетекают из кода в базу, критически важно для безопасности. Как только этот поток становится неконтролируемым, возникает уязвимость.
Исторический контекст: SQL-инъекции существуют столько же, сколько существуют веб-приложения с базами данных. Первое публичное упоминание термина приписывается Jeff Forristal под псевдонимом «rain.forest.puppy» в журнале Phrack за декабрь 1998 года. С тех пор SQLi стояли за крупнейшими взломами: TalkTalk 2015 (4 миллиона записей, штраф £400 тысяч от ICO + потери £77 миллионов в стоимости компании), Heartland Payment Systems 2008 (134 миллиона карточных номеров), Sony Pictures 2011, MOVEit Transfer 2023 (Clop ransomware группа эксплуатировала CVE-2023-34362, пострадали сотни компаний от BBC до правительства США). Не стек, не язык, не команда виновата — виноват забытый fmt.Sprintf в SQL-запросе.
Далее мы разберем «момент рождения» инъекции и увидим, как одна безобидная функция превращает ваш запрос в оружие хакера.
Продолжить чтение
Что бы прочитать модуль полностью, зарегистрируйтесь/войдите на платформу
Когда закончишь — отметь раздел, чтобы продолжить.