Безопасность и отказоустойчивость¶
TenderLens работает с недоверенными URL, JSON, XML, HTML, PDF и prompt-контекстом. Защита построена на нескольких независимых барьерах: один пропущенный check не должен сразу превращаться в SSRF, traversal, prompt injection или утечку секрета.
Границы доверия¶
flowchart LR
EXT["Недоверенные внешние данные"] --> URL["scheme + host + IP allowlist"]
URL --> SIZE["timeout + retry + byte limit"]
SIZE --> PARSE["Pydantic / defusedxml / safe HTML"]
PARSE --> STORE["constraints + safe filename"]
STORE --> RAG["relevance threshold + untrusted prompt"]
RAG --> OUT["sanitized API response"]
SSRF и redirect¶
ResilientHttpClient._validate_host() разрешает только http/https, требует host из закрытого allowlist, запрещает localhost, .local и неглобальные literal IP. Redirect обрабатывается вручную; каждый новый URL проходит ту же проверку. HTTP retries и redirect count являются разными счётчиками.
Что allowlist не делает
Текущая проверка запрещает literal private IP, но не выполняет собственный DNS resolution/pinning. Защита от DNS rebinding остаётся ограничением MVP; production-развёртывание должно дополняться egress firewall/proxy.
Файловая система¶
safe_filename() удаляет null byte, path components и опасные символы. Фактический путь включает UUID tender и attachment. Загрузка:
- предварительно проверяет
Content-Length, если он есть; - считает реальные байты независимо от заголовка;
- пишет в уникальный
.part; - одновременно вычисляет SHA-256;
- вызывает
flushиfsync; - заменяет target через
os.replace; - удаляет временный файл при любой ошибке.
Парсинг документов¶
- XML проходит через
defusedxml, чтобы отключить опасные сущности. - HTML-парсер отбрасывает
script,style,noscriptи извлекает только текст. - JSON декодируется как UTF-8 и повторно сериализуется в стабильный читаемый вид.
- PDF обрабатывается
pypdf; OCR и active content не исполняются. - неизвестный binary остаётся сохранённым, но не индексируется.
Prompt injection¶
build_rag_prompt() помещает найденный текст в секцию «КОНТЕКСТ» и системно объявляет документы недоверенными данными, а не инструкциями. Это уменьшает риск, но не является математической гарантией поведения модели. API также:
- не вызывает generation без результатов выше
MIN_RELEVANCE_SCORE; - передаёт максимум пять chunks;
- просит не добавлять факты из памяти;
- возвращает ровно использованные sources для проверки.
API-ключи и rate limit¶
Открытое значение показывается один раз. X-API-Key не попадает в структурированный лог: mask_mapping() маскирует известные secret-поля. Limiter использует PostgreSQL row lock, поэтому несколько API workers не расходуют один слот одновременно.
Событийная устойчивость¶
JetStream даёт семантику at least once, поэтому duplicate delivery считается нормой. Защита:
Nats-Msg-Id=event_idвключает broker deduplication window;indexed_hashделает повторную обработку дешёвой;- hash проверяется до и после AI;
ACKвыполняется только после DB commit;- временные ошибки получают
NAK, постоянные —TERM; ack_wait=300s,max_deliver=5ограничивают зависшие/ядовитые события.
Ошибки наружу и внутрь¶
Внешний клиент получает стабильный ErrorResponse с request_id, но без DSN, stack trace, local path или hash. Полный exception остаётся в JSON log и связывается по request_id, event_id, tender_id или source.
Production-gap checklist¶
- TLS termination и secret manager вместо локального
.env; - egress policy/DNS pinning для crawler;
- malware scanning вложений;
- OCR sandbox для image-only PDF;
- rotation и аудит API-ключей;
- метрики, tracing и alerting;
- dead-letter review после
max_deliver; - HNSW/IVFFlat и нагрузочное тестирование при росте chunks.
Политика disclosure и supported versions находятся в SECURITY.md.