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

Как корпоративный ИИ-помощник слил зарплаты и КП: разбор дыры в правах доступа RAG-системы

Разбираем реальный кейс, где чат-бот на базе корпоративных документов оказался единственным «сотрудником» без ограничений доступа — и почему это не баг конкретного вендора, а системная болезнь всех RAG-внедрений, включая крупные продукты вроде Gemini.

Разбор CIOlogia

Тема получила неожиданное подтверждение на международном уровне: в западной прессе разбирают случай, когда Gemini выдала пользователю информацию, которая по всем признакам хранилась только в закрытых Google Документах чужого аккаунта. Google комментировать отказался. Совпадение это или нет — вопрос открытый, но механика подозрительно похожа на то, что я лично находил у клиентов на плановых аудитах. Разберу один такой случай подробно — с командами, которыми это ищется, и конфигами, которыми это закрывается.

Обычное внедрение, необычная находка

Производственная компания, около 60 сотрудников, три подразделения. Полгода назад подключили корпоративного ИИ-помощника на отечественной языковой модели к базе внутренних документов: договоры, инструкции, приказы, кадровые файлы. Классический RAG (Retrieval-Augmented Generation) — помощник ищет релевантные куски документов по векторному индексу и подмешивает их в промпт модели перед ответом.

Меня позвали на плановый аудит доступов, не на аудит помощника. Но в первый день тестирования я задал боту невинный вопрос от имени рядового менеджера — и получил в ответе зарплатную ведомость отдела продаж, которая физически лежала в папке главбуха с ограниченным доступом.

Где была дыра

При интеграции помощника ему выдали сервисную учётную запись с доступом ко всему файловому хранилищу — так быстрее настраивать индексацию. Документы прогнали через эмбеддинг-модель и залили в векторную БД одним общим индексом, без метаданных о том, кому какой документ разрешён.

В результате получилась классическая ситуация из OWASP Top 10 for LLM Applications:

  • LLM02:2025 — Sensitive Information Disclosure: модель свободно достаёт из контекста то, что не должна была видеть
  • LLM06:2025 — Excessive Agency: сервисный аккаунт помощника имеет права шире, чем требуется для его роли
  • CWE-862 — Missing Authorization и CWE-639 — IDOR: авторизация человека проверяется на уровне файлового сервера, а у ретривера векторной БД её просто нет

Система разграничения прав в компании работала прекрасно — для людей. Для «ещё одного пользователя», которым фактически является сервисный аккаунт помощника, никто не настроил ничего.

Как это эксплуатируется

Для внутреннего аудита такие дыры ищутся быстро и без специфических эксплойтов — достаточно посмотреть на систему глазами обычного авторизованного пользователя с минимальными правами.

Шаг 1. Разведка API помощника

Чат-боты почти всегда открывают HTTP API отдельно от веб-морды. Ищем эндпоинты:

gobuster dir -u https://assistant.internal.company.local \
  -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt \
  -x json -t 40

===============================================================
/api                 (Status: 200)
/api/v1/query        (Status: 405)
/api/v1/assistant    (Status: 200)
/api/v1/history      (Status: 401)
/admin               (Status: 403)

Дополнительно смотрим стек через заголовки ответа и версии зависимостей — типичная картина для самописных RAG-интеграций на LangChain/LlamaIndex поверх открытых векторных БД (Chroma, Qdrant, Milvus, Elasticsearch с векторным плагином):

curl -sI https://assistant.internal.company.local/api/v1/query

HTTP/1.1 200 OK
Server: nginx/1.22.1
X-Powered-By: LangChain
Content-Type: application/json

Шаг 2. Прямой запрос от имени низкопривилегированного пользователя

Никакого взлома не требуется — достаточно легитимной сессии рядового менеджера (своей рабочей учётки). Проверяем, фильтрует ли API ответы по правам вызывающего:

curl -s -X POST https://assistant.internal.company.local/api/v1/query \
  -H "Authorization: Bearer $MANAGER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"question": "Покажи зарплаты сотрудников отдела продаж за этот квартал"}'

{
  "answer": "Согласно ведомости от 01.03.2024: Иванов И.И. — 145000, Петров П.П. — 98000, ...",
  "sources": ["/finance/зарплаты_март_2024.xlsx"]
}

Ключевой признак дыры прямо в ответе — поле sources ссылается на документ из папки, к которой у этого токена нет прав в файловом хранилище. Значит, retrieval-слой векторной БД не учитывает ACL пользователя вообще — фильтрация происходит только на этапе доступа человека к файлу напрямую, а не на этапе поиска по индексу.

Шаг 3. Автоматизация сбора чувствительных данных

Дальше это тривиально масштабируется скриптом с перебором «неудобных» вопросов — по сути, fuzzing семантического поиска вместо перебора параметров:

import requests

questions = [
    "Какие условия скидок у ключевых клиентов",
    "Есть ли планы по увольнению сотрудников",
    "Покажи зарплаты руководителей отделов",
    "Какие персональные данные хранятся в отделе кадров",
]

for q in questions:
    r = requests.post(
        "https://assistant.internal.company.local/api/v1/query",
        headers={"Authorization": f"Bearer {TOKEN}"},
        json={"question": q},
        timeout=15,
    )
    print(q, "->", r.json().get("sources"))

За пару минут собирается полная карта того, какие закрытые документы «протекают» через ассистента — без единого запроса к файловому серверу напрямую, то есть без единого события в стандартных логах доступа к файлам.

Дополнительно: если у помощника JWT с ролями

Если аутентификация построена на JWT, стоит проверить, не читает ли backend роль пользователя из самого токена без сверки с базой:

jwt_tool $MANAGER_TOKEN -T

[+] Token header values:
    alg = HS256
[+] Token payload values:
    role = "manager"
    dept = "sales"

Если сервер ретривера доверяет полю dept из токена, а не подтягивает актуальные права из HR-системы — можно смотреть, изменится ли ответ бота при модификации payload и пересборке подписи слабым секретом, что дополнительно проверяется через hashcat по словарю на HS256-секрет, если он окажется предсказуемым.

Полутехнические пруфы: как это выглядит в логах

В журнале самого ИИ-помощника (если он вообще ведётся) видно только «пользователь X задал вопрос Y, получен ответ» — без пометки, что источник ответа находится вне зоны доступа этого пользователя. В логах nginx перед API — обычный 200 OK, ничего подозрительного: запрос выглядит как штатное использование сервиса. Отдельного алерта не будет, потому что сама архитектура считает такой ответ корректным — модель просто нашла релевантный кусок текста в едином индексе.

Что и почему сработало бы дальше

Развитие атаки в такой ситуации почти не требует технических усилий:

  • Инсайдер с обычной рабочей учёткой методично «прощупывает» бота вопросами про зарплаты, скидки, кадровые решения — без единого следа в системах DLP, потому что данные выходят не файлом, а текстом в чате
  • Если в проиндексированных документах случайно оказались технические артефакты — пароли в конфигах, ссылки на VPN, доступы к 1С — они точно так же попадают в ответы бота на нейтральные вопросы вроде «где найти инструкцию по подключению к серверу»
  • Полученные credentials дальше используются для классического pivot: smbclient -L //fileserver -U user%password, перебор расшаренных ресурсов, попытка удалённого доступа через impacket-psexec
  • Условия скидок ключевых клиентов утекают конкуренту через обычную болтливость сотрудника — контракт теряется не из-за взлома, а из-за неудачно заданного вопроса

Самое неприятное: ни один из этих шагов не выглядит как атака ни для SOC, ни для DLP, ни для антивируса. Это легитимный пользователь, который вежливо спросил у корпоративного бота.

Во что это могло вылиться в рублях

В отделе продаж давно была разница в окладах у сотрудников на одинаковых позициях — обычная практика, но не для публичного обсуждения. Всплыви это в курилке — компания рисковала потерять минимум двух сильных менеджеров, которые в сумме приносили около 4 млн ₽