Внутренний вебхук выполнял любой SQL с правами суперпользователя - без пароля и токена
30 августа во время плановой проверки собственной инфраструктуры я решил вручную протестировать один из служебных маршрутов. Предположение о том, что внутренний endpoint автоматически защищён, проверкой безопасности не является. Поэтому был отправлен обычный HTTP-запрос с ноутбука:
```bash
curl -X POST https://example.com/webhook/mk-db-exec
-H "Content-Type: application/json"
-d '{"query":"SELECT current_user;"}'
```
Ответ оказался успешным: HTTP 200, без авторизационных заголовков, токенов и паролей. За маршрутом находился workflow в n8n: Webhook принимал JSON с произвольным SQL-текстом, передавал его в ноду Postgres, а затем возвращал результат клиенту.
Изначально этот механизм создавался как внутренний SQL-раннер для нескольких фоновых процессов маркетингового бота. Идея казалась удобной: не подключать отдельный драйвер и не хранить реквизиты базы в каждом скрипте, а отправлять запросы в уже настроенное соединение n8n. Однако подключение было создано от имени роли `postgres`, то есть суперпользователя. Такое решение оказалось быстрым на старте, но никто не вернулся к нему после завершения первичной настройки.
Права суперпользователя PostgreSQL означают гораздо больше, чем возможность читать или изменять отдельные таблицы. Такая роль способна работать с данными во всех схемах, включая клиентские записи, публикации и служебную информацию. Дополнительно была проверена команда `COPY ... TO PROGRAM`, позволяющая передавать результат SQL-запроса внешней программе операционной системы. Эта возможность предназначена для администрирования и доступна суперпользователю либо роли `pg_execute_server_program`. Вопрос о том, считать ли подобное поведение отдельной уязвимостью PostgreSQL, не меняет практического вывода: если злоумышленник может передать произвольный SQL, последствия зависят только от полномочий подключения.
Так проявляется типичная уязвимость выполнения SQL запросов: веб-интерфейс фактически становится удалённой консолью базы данных. Для полноценного аудита безопасности вебхуков недостаточно убедиться, что URL не опубликован на главной странице. Нужно проверить авторизацию, способ хранения адресов, доступность маршрута из разных сетей, содержимое логов и реальные права используемых учётных записей.
Почему "секретный" URL не является защитой
Следующим вопросом было понять, как посторонний человек вообще мог узнать адрес endpoint. Ответ нашёлся быстро: путь `/webhook/mk-db-exec` был прописан в 54 файлах на двух серверах. Он встречался в скриптах, резервных копиях и отдельных приватных репозиториях. Кроме того, URL использовался ещё в одном локальном сценарии - всего 55 мест.
Путь никогда не считался секретом. Это был обычный читаемый адрес, который копировали между скриптами в течение полутора месяцев. Поэтому первым шагом стала замена маршрута на случайную строку длиной 24 шестнадцатеричных символа. Все 55 потребителей были обновлены, а старый адрес после этого начал возвращать HTTP 404.
Однако на этом этапе возникла опасная иллюзия исправления. Случайный путь снижает вероятность случайного обнаружения маршрута перебором, но не помогает, если адрес уже попал в историю коммитов, старые архивы или резервные копии. У каждого, кто ранее имел доступ к этим данным, потенциально сохранялся рабочий путь к базе. Ротация URL не изменила главное - подключение по-прежнему выполнялось с правами суперпользователя.
Настоящая проблема заключалась не в предсказуемом имени маршрута, а в полном отсутствии модели авторизации. В n8n для Webhook доступны Basic Auth, Header Auth и JWT, но ни один вариант не был включён. При этом даже добавление заголовка с секретом устранило бы только несанкционированный вход. Злоумышленник с актуальным секретом всё равно получил бы возможности роли `postgres`.
Минимальные права вместо универсального подключения
Второй этап исправления касался самой базы. Была создана отдельная роль `mk_webhook`, которой предоставили только необходимые разрешения: доступ к конкретным схемам, таблицам и операциям, используемым фоновыми воркерами. Учётные данные ноды Postgres в n8n переключили с суперпользователя на новую роль.
После этого проверки повторили в условиях, максимально близких к атаке: с актуальным случайным URL, но с ограниченными полномочиями. Результаты выглядели так:
| Проверка | До исправления | После исправления |
|---|---|---|
| `SELECT current_user` | `postgres` | `mk_webhook` |
| Чтение `pg_authid` | Возвращались хеши паролей ролей | `permission denied` |
| `COPY ... TO PROGRAM` | Команда выполнялась | Доступ запрещён |
| Рабочий запрос к `mk_max.content_queue` | 167 строк | Выполняется |
| Рабочий запрос к `marketing_bot_v2.dm_threads` | 101 строка | Выполняется |
| Старый URL | HTTP 200 | HTTP 404 |
Ключевой результат - рабочие запросы фоновых процессов не изменились. Ограничение доступа не должно ломать бизнес-логику, если права рассчитаны на реальные операции, а не выданы "на всякий случай". Такой подход одновременно повышает безопасность PostgreSQL и управление доступом и уменьшает потенциальный ущерб при компрометации вебхука.
История также показывает, почему SQL-инъекция в веб-приложении не всегда выглядит как классическая вставка вредоносного фрагмента в параметр поиска. Иногда приложение само принимает целый SQL-текст и передаёт его в базу без фильтрации. В этом случае инъекция превращается в архитектурную проблему: небезопасный маршрут намеренно предоставляет слишком мощный интерфейс.
Что ещё пришлось сделать
Одной смены роли недостаточно. Для полноценной защиты внутренних API и вебхуков необходимо включить обязательную аутентификацию на самом маршруте, ограничить доступ по сети или через служебный шлюз, настроить rate limit и журналирование запросов. Секреты должны храниться в менеджере учётных данных, а не в исходном коде и резервных копиях.
Отдельно стоит проверить уже существующие логи и историю репозиториев. Если URL или пароль когда-либо были доступны широкой группе сотрудников, их следует считать скомпрометированными. Ротация должна касаться не только endpoint, но и паролей, токенов, credentials n8n и, при необходимости, ключей доступа к базе.
Полезно также добавить автоматические тесты безопасности в процесс развёртывания. Проверка должна подтверждать, что запрос без авторизации получает отказ, старые маршруты не работают, а подключение выполняется от имени ограниченной роли. Отдельный тест может пытаться обратиться к системным каталогам PostgreSQL и выполнить запрещённые операции.
Главный вывод прост: внутренний адрес - не механизм безопасности, а лишь часть маршрутизации. Надёжная схема строится сразу из нескольких уровней: аутентификация, сетевые ограничения, минимальные права базы, ротация секретов и регулярный контроль конфигурации. Если хотя бы один слой отсутствует, удобный служебный вебхук легко превращается в удалённый интерфейс администратора PostgreSQL.


