CIOlogia ИТ и безопасность · язык бизнеса, не технологий
← Все разборы Информационная безопасность / базы данных

Кто слил базу конкурентам: разбор утечки клиентских данных через один SELECT без LIMIT

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

12 июля 2026 г. Разбор CIOlogia

Собственник оптовой компании, торгующей стройматериалами, заметил странную закономерность: новый конкурент делает трём его постоянным клиентам предложения, которые совпадают с их же условиями до рубля — те же скидки, те же объёмы закупки. Совпадение исключалось. Кто-то взял базу целиком и обзвонил по списку.

Задача звучала просто: найти, кто и когда это сделал. Проблема в том, что база данных была настроена под продажи, а не под расследования: система показывала текущие данные, но не хранила историю обращений к ним. Классическая ситуация для СУБД на базе 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 и лимиты в приложении, чтобы полная выгрузка таблицы за один запрос была невозможна технически, а не только «по правилам».
  • Настроить алерты в реальном времени — уведомление руководителю в мессенджер при выгрузке выше порогового значения строк за одну сессию.
  • Автоматизировать отзыв доступа в день увольнения:
    REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA public FROM manager3;
    DROP ROLE manager3;
    а не через неделю, когда HR оформит бумаги.
  • Регулярно проверять план прав:
    \du+
    SELECT grantee, table_name, privilege_type
    FROM information_schema.role_table_grants
    WHERE table_name = 'clients';
    чтобы не накапливались избыточные разрешения по принципу «дали один раз и забыли».
Утечка почти никогда не выглядит как взлом — чаще это просто сотрудник с законным доступом, который забирает всё и сразу.

Стоимость такой настройки — разовая работа инженера на один-два дня, а не подписка на дорогой сервис DLP. Дальше собственник раз в неделю смотрит короткий отчёт: кто и сколько выгружал. Если история с полной выгрузкой базы повторится, об этом узнают в тот же день, а не через два месяца по звонкам от бывших клиентов.