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

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-инъекций, и мы пройдёмся по ним в следующих модулях.

Основные понятия:

  1. sql.DB: Это не само соединение, а пул соединений. Вы открываете его один раз при старте приложения.
  2. sql.Open: Функция для инициализации пула.
  3. db.Query: Используется для запросов, которые возвращают строки (SELECT).
  4. 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-запросе.

Далее мы разберем «момент рождения» инъекции и увидим, как одна безобидная функция превращает ваш запрос в оружие хакера.

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

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

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

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