Безопасность AI-агентов
Безопасная обработка файлов, которые получает AI-агент
Введение
Загруженный файл нельзя считать безопасным только потому, что его имя заканчивается на
.pdf или .png. Имя задаёт отправитель, а содержимое может оказаться
исполняемым файлом, повреждённым архивом или документом, рассчитанным на уязвимость парсера.
Даже обычный файл способен занять всю память, диск или процессор.
Первую проверку удобно строить вокруг фактического MIME-типа, но одной такой проверки недостаточно. Надёжная схема включает ограничения на нескольких уровнях: входной запрос, потоковое сохранение, карантин, проверку содержимого и запуск обработчика с минимальными правами.
Модель безопасного конвейера
- Веб-сервер отклоняет запрос, если его тело превышает общий лимит.
- Приложение сохраняет вложение в карантин потоково и повторно контролирует размер.
- Система определяет тип по содержимому, а не только по имени или заголовку запроса.
- Разрешённый файл передаётся отдельному обработчику без доступа к сети и секретам.
- Результат проверяется и только затем становится доступен AI-агенту.
Двойной контроль размера намеренный: ограничение веб-сервера защищает приложение от крупного запроса, а проверка в коде действует независимо от конфигурации инфраструктуры.
Шаг 1. Ограничьте запрос до приложения
Пример для Nginx ограничивает тело запроса двенадцатью мегабайтами. Значение немного больше допустимого размера файла, потому что multipart-запрос содержит служебные поля и границы.
server {
client_max_body_size 12m;
location /api/files {
proxy_pass http://agent_backend;
proxy_request_buffering on;
proxy_read_timeout 30s;
}
}
После изменения проверьте синтаксис и только затем перезагрузите конфигурацию:
sudo nginx -t
sudo systemctl reload nginx
Лимит должен соответствовать задаче. Если агент работает только с небольшими текстовыми инструкциями, разрешать десятки мегабайт незачем.
Шаг 2. Сохраняйте файл потоково в карантин
Ниже приведён самостоятельный пример на Python. Он не является готовым HTTP-обработчиком: функция принимает бинарный поток, который веб-фреймворк уже извлёк из запроса. Пример ограничивает файл десятью мегабайтами, не загружает его целиком в память и не использует исходное имя как путь.
from pathlib import Path
import hashlib
import os
import secrets
MAX_FILE_SIZE = 10 * 1024 * 1024
CHUNK_SIZE = 64 * 1024
QUARANTINE = Path("/var/lib/agent/quarantine")
class UploadRejected(Exception):
pass
def save_to_quarantine(source) -> tuple[Path, int, str]:
QUARANTINE.mkdir(mode=0o700, parents=True, exist_ok=True)
target = QUARANTINE / secrets.token_hex(24)
flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL
total = 0
digest = hashlib.sha256()
try:
fd = os.open(target, flags, 0o600)
with os.fdopen(fd, "wb") as output:
while True:
chunk = source.read(CHUNK_SIZE)
if not chunk:
break
total += len(chunk)
if total > MAX_FILE_SIZE:
raise UploadRejected("file is too large")
digest.update(chunk)
output.write(chunk)
if total == 0:
raise UploadRejected("empty file")
return target, total, digest.hexdigest()
except Exception:
try:
target.unlink(missing_ok=True)
finally:
raise
Случайное внутреннее имя устраняет обходы путей вроде ../../config и конфликты
имён. Исходное имя при необходимости храните только как метаданные после ограничения длины
и очистки управляющих символов.
Шаг 3. Проверьте фактический тип
Заголовок Content-Type присылает клиент, поэтому он годится лишь как подсказка.
На Linux фактический тип можно определить утилитой file. Передавайте путь
отдельным аргументом и завершайте список параметров через --.
file --brief --mime-type -- /var/lib/agent/quarantine/INTERNAL_ID
Пример строгого списка разрешённых типов:
ALLOWED_TYPES = {
"application/pdf",
"image/jpeg",
"image/png",
"text/plain",
}
Расширение, заявленный тип и тип по содержимому должны сравниваться. Несоответствие лучше отклонить или отправить на ручную проверку. Не пытайтесь автоматически «исправлять» подозрительные файлы переименованием.
Шаг 4. Изолируйте обработчик
Парсер документа не должен работать в процессе API или иметь доступ к ключам агента.
Следующий пример запуска контейнера без сети, с файловой системой только для чтения,
ограничениями памяти и числа процессов. Команда предполагает, что у вас уже есть
проверенный локальный образ agent-file-worker:1; имя образа здесь — пример.
docker run --rm \
--network none \
--read-only \
--cap-drop ALL \
--security-opt no-new-privileges \
--pids-limit 64 \
--memory 256m \
--cpus 1 \
--user 65534:65534 \
--mount type=bind,src=/var/lib/agent/quarantine/INTERNAL_ID,dst=/input/file,readonly \
--tmpfs /tmp:rw,noexec,nosuid,size=64m \
agent-file-worker:1
Не монтируйте в контейнер домашние каталоги, сокет Docker, конфигурацию приложения или хранилище секретов. Ограничьте время выполнения на уровне очереди задач либо внешнего супервизора. Если обработчик завис или превысил лимит, файл остаётся отклонённым.
Антивирусная проверка может быть дополнительным слоем, но она не заменяет изоляцию: сигнатуры не покрывают все новые угрозы, ошибки парсеров и ресурсные атаки.
Шаг 5. Передавайте агенту только контролируемый результат
Обработчик должен возвращать небольшой структурированный результат: извлечённый текст, число страниц, тип и код завершения. Установите отдельный лимит на объём результата. Например, документ на два мегабайта не должен превращаться в гигабайт текста.
{
"status": "accepted",
"detected_type": "application/pdf",
"size_bytes": 248731,
"sha256": "значение, вычисленное системой",
"text_truncated": false
}
Это пример структуры, а не реальный результат проверки. Хеш полезен для журналирования и поиска повторных загрузок, но сам по себе не показывает, безопасен ли файл.
Извлечённый текст также остаётся недоверенным вводом. Инструкции внутри документа не должны отменять системные правила агента, расширять его полномочия или автоматически запускать инструменты.
Проверка результата
Проверьте конвейер локальными файлами, созданными специально для этой проверки:
printf 'Обычный текст\n' > /tmp/agent-safe.txt
dd if=/dev/zero of=/tmp/agent-too-large.bin bs=1M count=11 status=none
cp /tmp/agent-safe.txt /tmp/not-really-an-image.png
file --brief --mime-type -- /tmp/agent-safe.txt
file --brief --mime-type -- /tmp/not-really-an-image.png
wc -c /tmp/agent-too-large.bin
Ожидаемое поведение системы:
- небольшой текстовый файл проходит проверку размера и определяется как текст;
- файл размером 11 МиБ отклоняется при лимите 10 МиБ;
- файл с ложным расширением
.pngне принимается как изображение; - обработчик не может обратиться к сети или прочитать каталоги хоста;
- при ошибке парсера агент получает безопасный статус, а не необработанную трассировку;
- отклонённый или обработанный файл удаляется по установленному сроку хранения.
Эти команды создают только локальные тестовые данные. Они проверяют ограничения и классификацию формата, но не моделируют вредоносный документ и не доказывают отсутствие уязвимостей.
Типовые ошибки
- Проверять только расширение
- Расширение легко подменить. Сопоставляйте его с типом, определённым по содержимому.
- Сначала читать весь файл в память
- Так ограничение срабатывает слишком поздно. Считайте байты при потоковой записи.
- Хранить загрузку под исходным именем
- Это создаёт риск обхода путей, перезаписи файлов и проблем с управляющими символами.
- Запускать парсер рядом с API
- Сбой или эксплуатация парсера затронет основной сервис и доступные ему секреты.
- Распаковывать архив без лимитов
- Маленький архив способен развернуться в большой объём данных или содержать опасные пути.
- Считать антивирус окончательной гарантией
- Он полезен как дополнительный сигнал, но не отменяет лимиты и изоляцию.
- Хранить карантин бессрочно
- Определите срок удаления и минимизируйте доступ к исходным пользовательским данным.
Ограничения подхода
Простая проверка MIME-типа не разбирает сложные контейнеры и полиглот-файлы. Для архивов, офисных документов, мультимедиа и моделей машинного обучения нужны отдельные правила: лимит числа объектов, глубины вложенности, суммарного распакованного размера и времени обработки.
Контейнер снижает последствия атаки, но не является абсолютной границей безопасности. Обновляйте среду выполнения и парсеры, фиксируйте версии образов, сокращайте набор установленных компонентов и сохраняйте журнал решений без содержимого файла и секретов.
Наконец, техническая безопасность не решает вопросы конфиденциальности и прав доступа. Перед сохранением вложений определите, кто может их читать, сколько они хранятся и как пользователь может запросить удаление.
Итоговый контрольный список
- Лимит запроса установлен на входном прокси.
- Приложение повторно считает размер потоково.
- Файлы получают случайные внутренние имена и права
0600. - Тип определяется по содержимому и сверяется со списком разрешённых форматов.
- Парсер отделён от API, сети, секретов и файловой системы хоста.
- Ограничены CPU, память, число процессов, время и размер результата.
- Агент получает результат обработки, а не прямой путь к загрузке.
- Настроены журналирование и автоматическое удаление карантина.
Другие практические схемы собраны в разделе руководств, а определения используемых понятий — в глоссарии.