30 KiB
module-05. Проектная спецификация message-safety
Статус: целевая production-спецификация MVP.
Канонические источники:README.md,arch-00-glossary.md,arch-01-system-architecture.md,arch-02-api-contracts.md,arch-03-docker-compose-blueprint.md,arch-04-settings-and-content.md,arch-05-agent-development-process.md,module-01-api-backend.md.
1. Назначение и приоритет
message-safety — внутренний сервис, который до отправки сообщения в Bitrix24 проверяет пользовательский текст, содержащиеся в нём ссылки и файлы из S3-quarantine.
Сервис закрывает угрозы, поступающие через пользовательское сообщение:
- управляющие и prompt-injection конструкции, направленные на оператора или последующую автоматическую обработку;
- опасные URL-схемы, URL с credentials и ссылки на private/link-local/metadata адреса;
- HTML/script-like payloads, способные стать активным содержимым при небезопасном отображении;
- подмену типа файла, несоответствие заявленного MIME фактическому формату и checksum;
- вредоносные файлы, обнаруживаемые антивирусными сигнатурами.
Спецификация детализирует архитектуру, но не меняет её. При конфликте приоритет имеют arch-00…arch-05. Канонические domain outcomes:
200 allow;403 deny;203 pendingс последующим sticky200или403.
Test-only правила по первому символу, случайные verdict и terminal 400 stub_final_error в production-контракт не входят.
2. Границы ответственности
2.1. Сервис отвечает за
- строгую валидацию internal DTO;
- нормализацию и rule-based проверку текста;
- извлечение и проверку ссылок;
- валидацию file metadata и фактического формата;
- чтение файла из S3-quarantine по read-only credentials;
- вычисление authoritative SHA-256;
- антивирусную проверку файла через ClamAV;
- выбор sync/async режима;
- создание и исполнение async safety tasks;
- sticky final verdict, verdict cache и audit в схеме
message_safety; - task coordination/cache/rate limits в Redis DB2;
- internal API, health, метрики, трассировку и безопасные JSON-логи.
2.2. Сервис не отвечает за
- JWT пользователя, согласия и авторизацию доступа пользователя к диалогу;
- edge/API rate limits;
- загрузку файла и выдачу presigned URL;
- запись пользовательского сообщения и статусов в
han_app; - copy/promote файла из quarantine в S3-data и удаление объекта;
- доставку в Bitrix24, realtime и пользовательский текст ошибки;
- анализ входящих сообщений оператора;
- ML-модерацию смысла, токсичности или правдивости текста.
Этими операциями владеет api-backend или соответствующий архитектурный модуль.
3. Threat model MVP
3.1. Текст и ссылки
| Угроза | Контроль | Результат |
|---|---|---|
| Prompt/control injection | Версионированные Unicode-aware rules | 403 deny |
| Попытка выдать текст за system/developer instruction | Нормализация + rule pack | 403 deny |
| Script/active-content payload | Правила для script, event-handler и опасных embedding-конструкций | 403 deny |
| Опасная URL-схема | Разрешены только http и https для распознанных web URL |
403 deny |
| URL с userinfo/credentials | Запрет user:password@host |
403 deny |
| SSRF-ссылка | DNS/IP classification, запрет private, loopback, link-local, multicast, unspecified и metadata endpoints | 403 deny |
| Обход Unicode/whitespace | NFKC, CRLF→LF, Unicode whitespace handling | Проверка нормализованного текста |
| ReDoS/DoS правилами | Линейные/ограниченные regex, лимиты текста, URL и времени | 400 или dependency error |
Rules не заменяют безопасный rendering. Frontend и Bitrix integration обязаны экранировать текст; safety является дополнительным барьером, а не HTML sanitizer.
3.2. Файлы
| Угроза | Контроль | Результат |
|---|---|---|
| Недопустимый размер/MIME | Сверка DTO с allow-list и лимитами | 403 deny |
| Подмена MIME | Magic-byte/content sniffing, сверка declared MIME | 403 deny |
| Подмена содержимого после complete | Полный SHA-256 против DTO checksum | 403 deny |
| Malware | ClamAV scan актуальными сигнатурами | 403 deny |
| Архивная бомба/ресурсное истощение | Лимиты размера, stream scan, ClamAV limits/timeouts | deny при policy hit; error при сбое |
| Polyglot/неоднозначный формат | Строгий формат detector и deny при mismatch/ambiguity | 403 deny |
| Повтор известного файла | Cache по SHA-256 + versions | Sticky cached verdict |
MVP принимает только типы из chat.attachments.allowed_extensions и chat.attachments.allowed_mime_types, при chat.attachments.max_size_mb. Расширение проверяет api-backend до вызова safety; message-safety независимо проверяет MIME и фактический формат байтов. Internal DTO не содержит имени файла, поэтому сервис не выводит расширение из object key.
3.3. Вне threat model MVP
- zero-day malware, отсутствующий в сигнатурах и эвристиках выбранного AV;
- OCR изображений и semantic analysis PDF;
- password-protected/encrypted containers: в MVP они запрещаются, если содержимое нельзя полностью проверить;
- DLP/поиск персональных данных, токсичности и запрещённой тематики;
- переход по пользовательской ссылке и анализ удалённой страницы.
4. Общий pipeline
flowchart TD
postCheck[POST_check] --> auth[Auth_and_DTO]
auth --> kind{content_kind}
kind -->|text| normalizeText[Normalize_text]
normalizeText --> textRules[Text_rules]
textRules --> linkRules[Link_pipeline]
linkRules --> syncVerdict[200_or_403]
kind -->|file| metadata[Metadata_validation]
metadata --> cache{SHA256_cache}
cache -->|hit| cached[Sticky_200_or_403]
cache -->|miss| task[203_and_task]
task --> worker[File_worker]
worker --> objectRead[S3_stream_and_SHA256]
objectRead --> formatCheck[Format_validation]
formatCheck --> avScan[ClamAV_scan]
avScan --> finalVerdict[Persist_sticky_verdict]
finalVerdict --> taskGet[GET_task_200_or_403]
Приоритет:
- service authentication, body/content-type/size и DTO validation;
- idempotency/fingerprint conflict;
- нормализация;
- обязательные проверки для соответствующего
content_kind; - любой deny имеет приоритет над allow;
- инфраструктурная ошибка не превращается ни в allow, ни в domain deny.
5. Текстовый pipeline
5.1. Нормализация
Pipeline детерминирован:
- принять только JSON UTF-8;
- заменить
CRLF/CRнаLF; - Unicode normalization
NFKC; - удалить leading Unicode whitespace;
- сохранить исходный регистр и punctuation для rules;
- ограничить текст internal DTO до 10 000 Unicode code points;
- вычислить SHA-256 нормализованного текста для correlation/cache без хранения текста.
content_kind=text требует поле text; пустой текст отклоняется upstream api-backend. content_kind=file допускает пустой text; текстовые rules тогда не запускаются.
5.2. Rule engine
Rules поставляются как статический read-only bundle приложения. Динамический код, regex или rule definitions из запроса запрещены.
Каждое правило содержит:
- стабильный
rule_id; reason_code;- severity;
- scope (
text,url,file_metadata); - action (
deny); rules_version;- тестовые positive/negative cases.
Начальный rule pack MVP:
text.prompt_instruction_override— конструкции вида «игнорируй предыдущие инструкции» и эквиваленты на поддерживаемых языках;text.prompt_role_impersonation— попытка обозначить пользовательский фрагмент как system/developer/tool instruction;text.prompt_secret_extraction— запрос раскрыть system prompt, credentials, tokens или внутренние инструкции;text.active_script—<script>, inline event handlers и эквивалентные active-content шаблоны;url.forbidden_scheme;url.credentials_present;url.private_destination.
Совпадение должно учитывать границы токенов и контекст, чтобы обычное обсуждение терминов не блокировалось простым substring match. Regex обязаны иметь ограниченную сложность и проходить ReDoS tests. Rule pack загружается и компилируется на startup; ошибка делает service not-ready.
Обычный текст без срабатывания обязательных rules получает 200 allow. Неуверенное отсутствие совпадения не является deny. Ошибка rule engine является 500, но не allow.
6. Pipeline ссылок
Сервис извлекает URL Unicode-aware parser-ом, а не одним regex.
Для каждой ссылки:
- ограничить количество ссылок в сообщении и длину URL;
- выполнить canonical parsing без автоматического исправления malformed URL;
- разрешить только
httpиhttps; - запретить userinfo/credentials;
- нормализовать IDNA host и отклонить malformed/confusable host;
- для literal IP применить IP policy;
- для hostname выполнить DNS resolve через доверенный resolver и проверить все A/AAAA;
- запретить loopback, RFC1918/ULA, link-local, multicast, unspecified, reserved ranges и cloud metadata endpoints.
Сервис не загружает содержимое URL и не следует redirects. Поэтому URL scan не создаёт исходящий HTTP SSRF. DNS failure или malformed URL, явно распознанный как ссылка, возвращает deny по policy; недоступность resolver для всех ссылок является dependency error.
Лимиты URL должны быть техническими env/settings, документированными в arch-04 до реализации.
7. Файловый pipeline
7.1. Fast path POST
До создания задачи сервис:
- валидирует attachment DTO;
- проверяет допустимый MIME и
size_bytes; - проверяет checksum
sha256:<64 lowercase hex>; - проверяет, что
quarantine_object_keyсоответствует разрешённому opaque key contract и не содержит traversal/control characters; - ищет cache по
(sha256, scanner_engine, signatures_version, rules_version).
Явно недопустимые metadata дают sync 403 deny. Cache hit возвращает sticky 200 или 403. Cache miss создаёт задачу и возвращает 203 pending.
Сервис не доверяет key как path и не строит произвольный URL. S3 client обращается только к configured quarantine bucket с virtual-hosted addressing и read-only credentials.
7.2. Worker
Worker:
- атомарно claim-ит pending task с lease;
- открывает S3 object stream с ограничением bytes/time;
- вычисляет authoritative SHA-256;
- сверяет фактический размер и checksum с DTO;
- определяет реальный формат по содержимому;
- сверяет detector result с declared MIME;
- запрещает encrypted/password-protected и неподдерживаемые containers;
- передаёт поток в
clamdчерез internal network; - сохраняет sticky final verdict и audit;
- записывает cache только для terminal результата;
- завершает lease.
Чистый файл получает allow только если успешно завершились все обязательные проверки. ClamAV FOUND, checksum mismatch, format mismatch, unsupported encrypted content или policy limit дают deny с отдельным rule_id.
ClamAV timeout, protocol error, недоступность S3/DB/Redis или потеря lease не являются deny. Task остаётся pending/retryable в пределах deadline; после исчерпания retry получает terminal infrastructure failure, который API отдаёт как 503, а не 403.
7.3. AV runtime
MVP использует отдельный clamd sidecar/service в private Docker network:
- порт не публикуется наружу;
- сигнатуры обновляет
freshclam; - readiness требует daemon PING и допустимый возраст signatures;
- limits согласованы с максимальным размером файла;
- контейнер non-root, read-only root filesystem где возможно, отдельный writable volume только для signatures/runtime;
- worker не передаёт в clamd object key, имя пользователя или иные PII.
Недоступность AV переводит /health/ready в 503 и запрещает новые file allow.
8. Internal API
Все /internal/safety/v1/* доступны только api-backend в private network.
Headers:
X-Service-Token: ${MESSAGE_SAFETY_SERVICE_TOKEN}
X-Request-ID: UUID/ULID; при отсутствии генерируется сервисом
traceparent: optional W3C
Content-Type: application/json; charset=utf-8
Token сравнивается constant-time. Missing/invalid token → 401 service_unauthorized без подсказок.
8.1. POST /internal/safety/v1/messages/check
{
"message_id": "uuid",
"content_kind": "text",
"text": "Текст сообщения",
"attachment": null
}
{
"message_id": "uuid",
"content_kind": "file",
"text": "",
"attachment": {
"attachment_id": "uuid",
"quarantine_object_key": "opaque",
"mime_type": "application/pdf",
"size_bytes": 12345,
"checksum": "sha256:<64-lowercase-hex>"
}
}
Неизвестные поля запрещены. text и file — строгий discriminated union.
200:
{
"verdict": "allow",
"rule_id": "safety.all_checks_passed",
"rules_version": "2026-01-01"
}
403:
{
"verdict": "deny",
"rule_id": "file.malware_detected",
"reason_code": "message_blocked",
"rules_version": "2026-01-01"
}
203:
{
"verdict": "pending",
"task_id": "uuid",
"poll_after_ms": 2000,
"expires_at": "2026-07-29T15:00:00Z",
"rules_version": "2026-01-01"
}
8.2. GET /internal/safety/v1/messages/tasks/{task_id}
- running/retryable →
203 pending; - sticky allow →
200 allow; - sticky deny →
403 deny; - malformed UUID →
400 validation_error; - unknown/expired →
404 task_not_found; - dependency failure →
503.
После final verdict повторный GET возвращает тот же HTTP status, verdict, rule_id и rules_version. Task не удаляется до retention expiry.
8.3. Error envelope
{
"error": {
"code": "validation_error",
"message": "Request is invalid",
"request_id": "uuid",
"details": {}
}
}
| HTTP | Code/смысл | Retry |
|---|---|---|
400 |
malformed DTO/path | нет без исправления |
401 |
service_unauthorized |
нет без исправления secret |
403 |
domain deny |
нет |
404 |
task_not_found |
dependency reconciliation |
409 |
safety_request_conflict |
нет |
429 |
rate_limit_exceeded |
по Retry-After |
500 |
internal_error |
да |
503 |
dependency/scanner/storage unavailable | да |
Domain 403 не считается circuit breaker failure.
9. Idempotency и state model
Fingerprint = SHA-256 canonical normalized DTO без token/request-id/trace headers.
- одинаковый
(message_id, fingerprint)возвращает тот же активный task или sticky final verdict; - тот же
message_idс другим fingerprint →409 safety_request_conflict; - concurrent duplicate создаёт одну task;
- final verdict изменять запрещено;
- transient error не кэшируется как allow/deny;
- повторный worker delivery безопасен через task state compare-and-set.
Task states:
pending -> processing -> allowed
-> denied
processing -> pending (retry with lease expiry)
processing -> failed (infrastructure retries exhausted)
allowed, denied, failed terminal и sticky. failed не является safety deny.
10. Хранение данных
10.1. PostgreSQL schema message_safety
safety_tasks:
id,message_id,attachment_id;- request fingerprint и content hash;
- task status, attempts, lease owner/until, next attempt/deadline;
- declared MIME/size/checksum;
- verdict,
rule_id,reason_code; - rules/scanner/signatures versions;
- timestamps и common audit fields.
verdict_cache:
- content SHA-256;
- rules/scanner/signatures versions;
- sticky verdict и rule;
- expiry/created timestamps;
- unique versioned cache key.
safety_audit:
- request/task identifiers;
- event (
received,task_created,rule_hit,scan_completed,dependency_failed); - verdict/rule/version;
- duration and technical error category;
- timestamp.
Raw message text, file bytes, object key, filename, service token и AV stream в PostgreSQL не сохраняются. Для correlation используются UUID и cryptographic hashes.
10.2. Redis DB2
Redis используется для:
- short-lived task lookup;
- dedup/reservation coordination;
- hot verdict cache;
- leases/locks;
- internal rate limits.
PostgreSQL остаётся durable source of truth. Потеря Redis не должна менять sticky final verdict; сервис восстанавливает state из PostgreSQL. Redis unavailable делает service not-ready и file task creation недоступным.
TTL task должен превышать MESSAGE_SAFETY_TASK_POLL_MAX_SEC плюс recovery/network margin. Verdict cache TTL задаётся отдельно и инвалидируется версиями rules/scanner/signatures.
11. Согласование с api-backend
POST check
200 -> allow -> text accepted or file promote -> Bitrix delivery
403 -> blocked/rejected -> no Bitrix -> public 422 message_blocked
203 -> persist han_app.safety_tasks -> poll GET
GET 203 -> continue
GET 200 -> allow branch
GET 403 -> deny branch
timeout/5xx -> public 503/504, no promote, no Bitrix
message-safety не перемещает и не удаляет S3 object. На deny это идемпотентно делает api-backend. Клиент не получает internal 203.
12. Security controls
- internal network only; endpoint не публикуется через nginx и host port;
- unique service token только из env/secret mount;
- strict DTO, body/text/URL/file limits и запрет unknown fields;
- read-only S3-quarantine access, без list/write/delete;
- no arbitrary URL fetch, no redirect following;
- egress allow-list только DNS, S3, PostgreSQL, Redis, OTLP и clamd по назначению;
- parameterized SQL и least-privilege DB role только на schema
message_safety; - Redis ACL только DB2 и prefixes
han:safety:*; - dependency pinning, SBOM/image scanning;
- non-root, read-only root fs, tmpfs
/tmp, dropped capabilities, no-new-privileges; - OpenAPI UI выключен в production;
- безопасные generic errors без stack/internal addresses;
- правила и AV signatures обновляются только trusted deployment process.
13. Observability и privacy
JSON logs:
timestamp,level,service.name=message-safety,module,event;request_id,trace_id,span_id, route, status, duration;- verdict,
rule_id, rules/scanner/signatures version; - task state, attempt, age/size bucket;
- dependency/error code.
Запрещено логировать message text, extracted URL целиком, file bytes, object key/name, checksum целиком, DTO body и service token. URL host и hash допустимы только при утверждённой retention policy; по умолчанию логируется category/hash.
Metrics:
- request rate/latency/status по route;
- checks/verdicts по
content_kind,allow|deny|pending|error; - rule hits по low-cardinality
rule_id; - task queue/age/attempts/lease conflicts/timeouts;
- scan latency/bytes buckets/AV outcome;
- signatures age/version info;
- cache hit/miss;
- PostgreSQL/Redis/S3/ClamAV latency and errors;
- auth rejects/rate limit/readiness.
Telemetry collector unavailable не влияет на safety verdict и readiness.
14. Health
GET /health/live проверяет только process/event loop.
GET /health/ready проверяет:
- settings, secret и rules bundle;
- PostgreSQL read/write в schema
message_safety; - Redis DB2 PING и prefixed SET/GET/DEL;
- worker heartbeat/lease processing;
- S3-quarantine Head/Get read permission на безопасный canary object;
- ClamAV PING и допустимый возраст signatures;
- OpenAPI schema availability.
Критическая dependency down → 503:
{
"status": "not_ready",
"components": {
"postgres": "ok",
"redis": "ok",
"s3_quarantine": "ok",
"worker": "ok",
"antivirus": "down",
"rules": "ok"
}
}
Health не требует service token внутри private ops network и не раскрывает credentials/hostnames.
15. Configuration
Канонические существующие env:
APP_ENV=production-like
MESSAGE_SAFETY_DATABASE_URL=postgresql://message_safety_app:<secret>@<host>:5433/han_chat
MESSAGE_SAFETY_REDIS_URL=redis://message_safety:<secret>@redis:6379/2
MESSAGE_SAFETY_SERVICE_TOKEN=<secret>
MESSAGE_SAFETY_RULES_VERSION=2026-01-01
MESSAGE_SAFETY_POST_TIMEOUT_SEC=5
MESSAGE_SAFETY_TASK_POLL_INTERVAL_SEC=2
MESSAGE_SAFETY_TASK_POLL_MAX_SEC=300
MESSAGE_SAFETY_FILE_SCAN_TIMEOUT_SEC=60
SELECTEL_S3_ENDPOINT_URL=https://s3.storage.selcloud.ru
SELECTEL_S3_BUCKET_QUARANTINE=han-chat-quarantine
SELECTEL_S3_QUARANTINE_READ_ACCESS_KEY=<secret>
SELECTEL_S3_QUARANTINE_READ_SECRET_KEY=<secret>
OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317
ClamAV endpoint, signature max age, worker concurrency, retry/lease, task/cache TTL, text/URL limits и detector version требуют добавления в arch-04 до реализации. Бизнес allow-list типов/размера остаётся в app_settings; api-backend передаёт согласованные metadata, а safety использует versioned runtime snapshot.
Startup отклоняет placeholders, insecure production defaults, несовместимые timeout/TTL и отсутствующие обязательные параметры.
16. OpenAPI
message-safety/openapi.yaml OpenAPI 3.1 обязателен и содержит:
X-Service-Tokensecurity scheme;- common request/trace headers;
- strict discriminated union text/file;
- exact POST responses
200/203/400/401/403/409/429/500/503; - exact GET responses
200/203/400/401/403/404/429/500/503; - domain deny отдельно от error envelope;
- UUID/checksum/max length examples;
- health contracts.
Runtime schema сравнивается с committed artifact contract test. Terminal 400 stub_final_error отсутствует.
17. Тестовая матрица
Unit
- NFKC, CRLF, Unicode whitespace, max length;
- positive/negative cases каждого text rule;
- границы токенов и false-positive corpus;
- ReDoS/time budget всех regex;
- URL parsing, IDNA, schemes, credentials и IP ranges IPv4/IPv6;
- DTO union, fingerprint и rule priority;
- MIME/magic/checksum decision table;
- sticky state transitions;
- log redaction.
Integration
- PostgreSQL migration/constraints/idempotency/audit;
- Redis DB2 reserve/cache/TTL/lease concurrency/recovery;
- S3 read-only stream, object changed/missing/timeout;
- ClamAV clean, EICAR, FOUND, timeout, daemon down, stale signatures;
- full SHA-256 and size mismatch;
- duplicate concurrent request and worker retry;
- dependency recovery without changed final verdict.
Contract
- text allow and each deny category;
- file cache hit,
203then sticky200,203then sticky403; - no random/non-sticky outcomes;
- malformed
400never treated as deny; - auth missing/wrong/correct;
- request-id and trace propagation;
- api-backend maps only domain
403to public422 message_blocked; - deny/timeout never calls Bitrix and never promotes quarantine;
- OpenAPI runtime parity.
Security/failure
- EICAR and safe corpus;
- malformed/polyglot/encrypted files;
- decompression and parser resource limits;
- URL private/link-local/metadata/IPv4-mapped-IPv6 cases;
- DNS rebinding simulation;
- no raw text/token/URL/object key/checksum in logs/traces/errors;
- S3 credentials cannot list/write/delete;
- Redis ACL and PostgreSQL schema isolation;
- telemetry outage does not affect verdict.
18. Definition of Done
- production
200/203/403contract реализован без stub divergence; - text rules и URL pipeline покрывают threat model и false-positive corpus;
- files проходят metadata, authoritative SHA-256, format detector и ClamAV;
- final task verdict sticky и durable;
- idempotency/concurrency/recovery доказаны тестами;
- PG schema
message_safety, Redis DB2 и S3 read-only работают по least privilege; - cache versioned rules/scanner/signatures и не сохраняет transient errors;
- api-backend mapping allow/deny/timeout проверен end-to-end;
- blocked/failed content не попадает в Bitrix и не promote-ится;
- health/readiness отражает PG/Redis/S3/workers/ClamAV/rules;
- OpenAPI 3.1 и runtime parity зелёные;
- logs/metrics/traces не содержат содержимое сообщений, файлов и secrets;
- hardened containers запускаются без public port;
- runbook описывает signature update, stale signatures, AV outage, retry и rollback.
19. Переход с текущей заглушки и follow-up
Текущая реализация в codebase/backend/message-safety/ остаётся test stub и не соответствует этой production-спецификации. Для перехода отдельной задачей необходимо:
- удалить правила
ф/Ф, digit task и random RNG; - удалить terminal
400 stub_final_error; - реализовать sticky canonical
403для async deny; - подключить PostgreSQL schema
message_safety, Redis DB2, S3 read-only и ClamAV workers; - добавить migrations, полный OpenAPI и тестовую матрицу;
- синхронизировать
module-01-api-backend.mdи код api-backend, удалив test-only mappingstub_final_error; - синхронизировать
module-09-observability.mdи dashboards, удалив stub distribution panels/маркировку; - добавить новые technical env и ClamAV deployment contract в
arch-04/arch-03до production реализации.
До выполнения перехода контейнер должен быть явно маркирован как stub и не считаться production security control.