Кто слил базу конкурентам: разбор утечки клиентских данных через один SELECT без LIMIT
Оптовая компания потеряла трёх клиентов и 3,5 млн рублей за два месяца — а виновного нашли за один день, включив аудит там, где база данных штатно ничего не запоминала.

Собственник оптовой компании, торгующей стройматериалами, заметил странную закономерность: новый конкурент делает трём его постоянным клиентам предложения, которые совпадают с их же условиями до рубля — те же скидки, те же объёмы закупки. Совпадение исключалось. Кто-то взял базу целиком и обзвонил по списку.
Задача звучала просто: найти, кто и когда это сделал. Проблема в том, что база данных была настроена под продажи, а не под расследования: система показывала текущие данные, но не хранила историю обращений к ним. Классическая ситуация для СУБД на базе PostgreSQL без включённого расширенного аудита — включая отечественные форки вроде Postgres Pro Enterprise, где по умолчанию тоже работает только базовое логирование.
Где была дыра
Доступ к клиентской базе имели 15 менеджеров и бухгалтер — у каждого своя учётная запись в СУБД или общий сервисный логин через 1С/CRM-прослойку. Права были настроены по принципу «дать всем всё, чтобы никто не жаловался»: единая роль sales_manager с SELECT на всю таблицу clients, без построчных ограничений (Row Level Security не применялся).
Штатный PostgreSQL в такой конфигурации пишет в лог только то, что разрешено параметром log_statement — по умолчанию это none или в лучшем случае ddl (только структурные изменения). Обычные SELECT-запросы, даже если они выгружают всю таблицу, в журнал не попадают вообще. Проверить это можно одной командой:
psql -U postgres -c "SHOW log_statement;"
log_statement
---------------
none
(1 row)
То есть система физически не могла ответить на вопрос «кто скачал базу» — этой информации просто не существовало. Отдельная функция полноценного аудита (кто, когда, какая роль, сколько строк вернул запрос) в чистом PostgreSQL отсутствует как класс — это либо расширение pgaudit, либо специализированные модули вроде pg_proaudit в Postgres Pro Enterprise.
Сколько это стоило
Пока шло разбирательство, компания потеряла около 3,5 млн рублей выручки за два месяца — сумма контрактов с ушедшими клиентами плюс скидки, которые пришлось дать оставшимся под угрозой ухода. К этому добавился репутационный эффект: часть клиентов решила, что раз их данные утекли один раз, компании вообще нельзя доверять чувствительную информацию об объёмах и условиях закупки.
Как это эксплуатируется
В этом кейсе внешнего взлома не было — сработал легитимный доступ инсайдера. Но стоит показать полную цепочку, потому что ровно та же дыра (отсутствие аудита + широкие права SELECT) одинаково опасна и для инсайдера, и для внешнего атакующего, получившего пароль от учётки менеджера.
Шаг 1. Разведка периметра
nmap -sV -p 5432 --script=pgsql-brute 192.168.10.15
PORT STATE SERVICE VERSION
5432/tcp open postgresql PostgreSQL DB 14.9 - 15.4
Если СУБД смотрит наружу или доступна из офисной сети без сегментации, порт 5432 виден любому, кто просканировал подсеть.
Шаг 2. Подбор или использование валидных учётных данных
hydra -l manager3 -P rockyou.txt postgres://192.168.10.15
[5432][postgresql] host: 192.168.10.15 login: manager3 password: Ivanova2023
В реальном кейсе пароль подбирать не пришлось — доступ был штатный, у уволившегося сотрудника учётка ещё две недели оставалась активной.
Шаг 3. Одна команда — вся база
psql -h 192.168.10.15 -U manager3 -d sales_db \
-c "COPY (SELECT * FROM clients) TO STDOUT WITH CSV HEADER" > clients_full.csv
wc -l clients_full.csv
4218 clients_full.csv
Или тем же результатом через pg_dump конкретной таблицы:
pg_dump -h 192.168.10.15 -U manager3 -t clients -d sales_db > clients_dump.sql
Обычный менеджер за рабочий день открывает карточки 5–10 своих клиентов — это десятки коротких SELECT по WHERE id = .... Здесь же один запрос вернул все 4218 строк без единого фильтра. Это и есть сигнатура утечки: не количество подключений, а объём данных за один вызов.
Шаг 4. Эксфильтрация
Дальше файл в пару кликов уходит на личную почту, в облако или на флешку — контроля DLP на рабочих станциях не было, поэтому этот этап тоже прошёл незаметно.
Что и почему сработало бы дальше
Если бы расследование не началось, у атакующего (или недобросовестного бывшего сотрудника) был бы полный набор данных для системного демпинга: контакты, объёмы закупки, персональные скидки — то есть готовый прайс-лист конкурента с указанием, кого и на сколько недожимать. Плюс телефоны и e-mail клиентов — материал для отдельной атаки: фишинговых писем «от лица» знакомой компании с просьбой сменить реквизиты оплаты, что уже прямые финансовые потери у клиентов, а не только у самой компании.
Как я нашёл виновного
Первым делом включил расширенный аудит. В Postgres Pro Enterprise для этого есть модуль pg_proaudit — в отличие от штатного логирования он пишет не просто факт запроса, а метаданные: роль, время, количество возвращённых строк, исходную сессию.
-- в postgresql.conf
shared_preload_libraries = 'pg_proaudit'
pg_proaudit.log_statement = 'all'
pg_proaudit.log_rows_threshold = 100 -- сигнал при выгрузке от 100 строк
После перезапуска СУБД и накопления истории поиск аномалии занял один запрос к журналу аудита (в терминах LogsQL/pgpro-otel-collector это выглядит примерно так):
* | filter role="manager3" AND rows_returned > 1000
| stats count() by session_time, query
Результат — один запрос за день до заявления об увольнении, вернувший всю таблицу целиком, вместо привычных 5–10 карточек. Дальше сопоставление со временем увольнения и списком клиентов, ушедших к конкуренту, закрыло вопрос «кто» без каких-либо предположений.
Как закрыть
- Включить аудит на уровне СУБД, а не полагаться на логи приложения:
shared_preload_libraries = 'pgaudit' pgaudit.log = 'read, write, role' pgaudit.log_relation = on - Ограничить доступ Row Level Security — каждый менеджер видит только своих клиентов:
ALTER TABLE clients ENABLE ROW LEVEL SECURITY; CREATE POLICY manager_own_clients ON clients USING (manager_id = current_setting('app.current_manager_id')::int); - Ограничить объём одной выборки для ролей с широкими правами, например через
statement_timeoutи лимиты в приложении, чтобы полная выгрузка таблицы за один запрос была невозможна технически, а не только «по правилам». - Настроить алерты в реальном времени — уведомление руководителю в мессенджер при выгрузке выше порогового значения строк за одну сессию.
- Автоматизировать отзыв доступа в день увольнения:
а не через неделю, когда HR оформит бумаги.REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA public FROM manager3; DROP ROLE manager3; - Регулярно проверять план прав:
чтобы не накапливались избыточные разрешения по принципу «дали один раз и забыли».\du+ SELECT grantee, table_name, privilege_type FROM information_schema.role_table_grants WHERE table_name = 'clients';
Утечка почти никогда не выглядит как взлом — чаще это просто сотрудник с законным доступом, который забирает всё и сразу.
Стоимость такой настройки — разовая работа инженера на один-два дня, а не подписка на дорогой сервис DLP. Дальше собственник раз в неделю смотрит короткий отчёт: кто и сколько выгружал. Если история с полной выгрузкой базы повторится, об этом узнают в тот же день, а не через два месяца по звонкам от бывших клиентов.