4. SQL Injection внутри JSON (PostgreSQL ->>)
SQL Injection внутри JSON (Go + PostgreSQL)
Формат JSONB в PostgreSQL позволяет хранить структурированные данные и выполнять по ним эффективный поиск. Для Go-разработчика это звучит заманчиво, но именно здесь часто повторяются старые ошибки. Неправильное использование JSON-операторов при построении запросов открывает новые векторы для классических атак.
JSONB появился в PostgreSQL 9.4 (2014 год) и быстро стал популярным выбором для гибридных схем, где часть данных строго структурирована (id, created_at, status), а часть варьируется от записи к записи (settings, metadata, custom_fields). Вместо того чтобы пихать неструктурированные данные в MongoDB, Go-команды стали хранить их прямо в PostgreSQL — одна СУБД, один транзакционный контекст, привычные индексы. Однако вместе с гибкостью JSONB пришли новые SQL-операторы: ->, ->>, @>, ?, jsonb_path_query — и каждый из них может стать точкой инъекции, если использовать fmt.Sprintf вместо параметров.
Аналогия: представь библиотеку, где у каждой книги есть стандартная карточка (автор, год, ISBN) и приложенный конверт с произвольными заметками — рецензиями, пометками владельца, вырезками. JSONB — это такой конверт. Поиск «найди мне книги, где в заметках упоминается Достоевский» удобен, но если ты позволяешь читателю самому формулировать SQL-запрос для поиска по конверту, он может незаметно дописать «и заодно покажи мне каталог редких книг из хранилища». Параметризация — это «фильтр» библиотекаря: он принимает запрос только в строго определённом формате и сам составляет безопасную команду.
Посмотрим, как хакер может использовать поиск по JSON-ключам для извлечения всей базы данных.
1. Механика: JSON-операторы как вектор атаки
Перед нами типичный сценарий «гибкого API»: пользователь выбирает в UI, по какому полю JSON-документа выполнить поиск, и фронтенд передаёт это поле как query-параметр. На бэкенде разработчик решает, что раз поле — это идентификатор колонки в JSONB, а не значение, то его «нельзя» закрыть параметром, и собирает запрос через fmt.Sprintf. Это ошибка номер один в работе с JSONB.
Рассмотрим типичный динамический запрос в Go-приложении (например, на Fiber):
Разработчик берет имя поля из запроса: key := c.Query("field") (например, name или email).
И строит SQL: query := fmt.Sprintf("SELECT data->>'%s' FROM events", key)
Хакер вместо простого имени ключа присылает:
?field=name' UNION SELECT password FROM users--
Что в итоге попадает в базу:
// ❌ СМЕРТЕЛЬНО УЯЗВИМО: Внедрение через JSON-синтаксис
query := "SELECT data->>'name' UNION SELECT password FROM users--' FROM events"
База данных интерпретирует это как объединение двух совершенно разных запросов: первый ищет по JSON-полю, а второй — крадет пароли из таблицы users.
Обрати внимание: атакующий ничего не делал с самим JSON-документом. Он вставил инъекцию в имя ключа JSON, которое разработчик считал «безопасным идентификатором». На самом деле же это просто строка, попадающая в SQL-команду через конкатенацию — те же самые правила, что и для классического WHERE name='...'. Парсер PostgreSQL воспринимает результирующую строку как SQL-команду, и единственный его критерий валидности — это синтаксическая корректность. Если атакующий правильно балансирует кавычки и закрывает структуру комментарием --, запрос пройдёт компиляцию и выполнится.
2. Ловушка «безопасного» формата
Часто разработчики думают: «Я же не трогаю WHERE, я просто указываю ключ в JSON-объекте». Это опасное заблуждение. Для SQL-парсера нет принципиальной разницы, в какую часть строки внедряется вредоносный код. Любое место, где используется конкатенация и есть риск нарушения структуры кавычками — это потенциальная уязвимость.
3. Сравнение классической и JSON-инъекции
| Тип атаки | Вектор внедрения | Пример нагрузки |
|---|---|---|
| Классика | WHERE name = '%s' |
' OR 1=1-- |
| JSONB | data->>'%s' |
name' UNION SELECT...-- |
4. Как распознать опасность в Go-коде
Использование операторов ->>, ->, @> внутри функций форматирования строк (fmt.Sprintf) или сырых запросов (db.Raw) — это верный признак уязвимости. Работа с JSONB-полями в Go обязательно должна сопровождаться использованием параметров ($1), так же как и в любых других частях SQL-запроса: data->>$1.
Корректный паттерн: ключ JSON-документа всегда передаётся как параметр, никогда — как часть SQL-строки. Запрос db.QueryRow("SELECT data->>$1 FROM events WHERE id=$2", fieldName, eventID) безопасен независимо от того, что находится в fieldName. PostgreSQL правильно декодирует параметр $1 как строку и применит JSON-оператор ->> к этому ключу, а попытка инъекции вроде name' UNION SELECT... просто превратится в поиск ключа с буквальным именем name' UNION SELECT..., который, естественно, ничего не найдёт.
Для linter'ов есть готовые правила: gosec ловит fmt.Sprintf с SQL-подобными строками (правило G201, G202); sqlc на этапе компиляции отвергает любые динамические колонки в запросах. Используйте эти инструменты в CI/CD пайплайне — они дешевле, чем post-mortem после инцидента.
JSONB — мощный инструмент, но он требует такой же строгой дисциплины, как и обычные таблицы. Без параметризации любой ваш «гибкий поиск» по JSON превращается в прямой путь к утечке данных.
Эволюция уязвимостей в JSONB: с появлением jsonb_path_query в PostgreSQL 12 появился новый класс инъекций — JSON Path Injection. Атакующий, контролирующий путь поиска, может выйти за пределы документа и обратиться к произвольным метаданным. CVE-2021-32672 в PostgreSQL описывает уязвимость в обработке @? оператора, которая позволяла читать произвольные файлы через специально сконструированный JSONPath. Урок тот же: динамические части SQL-запроса должны быть либо параметрами, либо строго whitelisted значениями из закрытого списка.
Соответствие стандартам: OWASP Top 10 A03:2021 Injection (включает все варианты, не только классический SQL); CWE-89; PCI-DSS 6.5.1.
В следующем подмодуле мы разберем одну из самых сложных тем — уязвимости в блоках сортировки (ORDER BY), которые невозможно закрыть стандартными плейсхолдерами.
Продолжить чтение
Что бы прочитать модуль полностью, зарегистрируйтесь/войдите на платформу
Когда закончишь — отметь раздел, чтобы продолжить.